From 6af68e6ef5ddd040276ff93508f8aff2531abea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Mon, 21 Sep 2026 18:01:40 +0300 Subject: [PATCH] =?UTF-8?q?Barkod=20ve=20Image=20Capture=20=C3=B6zelli?= =?UTF-8?q?=C4=9Fi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lowcode-reference.instructions.md | 3 +- .github/instructions/lowcode.instructions.md | 16 +- README.md | 6 +- .../GridOptionsDto/ImageUploadOptionsDto.cs | 6 + .../Migrations/LanguagesData.json | 192 ++++++++++++++++++ .../Editors/EditorScript.cs | 8 + .../Editors/EditorScriptRule.cs | 6 + .../App.DeveloperKit.CustomComponents.json | 94 +-------- ui/package-lock.json | 49 ++++- ui/package.json | 3 +- .../componentEditor/runtimeNodes.ts | 2 + .../BarcodeScanner/BarcodeScanField.tsx | 83 ++++++++ .../BarcodeScanner/BarcodeScannerDialog.tsx | 111 ++++++++++ .../BarcodeScanner/BarcodeScannerHost.tsx | 33 +++ .../components/shared/BarcodeScanner/index.ts | 3 + .../shared/BarcodeScanner/scanBarcode.ts | 22 ++ .../shared/BarcodeScanner/useBarcodeReader.ts | 123 +++++++++++ ui/src/components/shared/index.ts | 1 + ui/src/components/visualDesigner/catalog.ts | 35 ++++ ui/src/proxy/form/models.ts | 2 + ui/src/utils/barcode.ts | 119 +++++++++++ ui/src/utils/device.ts | 35 ++++ ui/src/utils/editorScriptRuntime.ts | 42 ++++ ui/src/views/Views.tsx | 2 + .../editor-options/optionSpecs.ts | 13 ++ .../editor-options/presets.ts | 23 +++ .../editor-script/scriptRecipes.ts | 28 +++ .../developerKit/VisualComponentDesigner.tsx | 7 + .../editors/ImageUploadEditorComponent.tsx | 38 +++- .../editors/ImageUploadEditorComponent.tsx | 40 +++- ui/src/views/list/shared/editingForm.ts | 20 +- ui/src/views/list/useToolbar.tsx | 58 +++++- 32 files changed, 1120 insertions(+), 103 deletions(-) create mode 100644 ui/src/components/shared/BarcodeScanner/BarcodeScanField.tsx create mode 100644 ui/src/components/shared/BarcodeScanner/BarcodeScannerDialog.tsx create mode 100644 ui/src/components/shared/BarcodeScanner/BarcodeScannerHost.tsx create mode 100644 ui/src/components/shared/BarcodeScanner/index.ts create mode 100644 ui/src/components/shared/BarcodeScanner/scanBarcode.ts create mode 100644 ui/src/components/shared/BarcodeScanner/useBarcodeReader.ts create mode 100644 ui/src/utils/barcode.ts create mode 100644 ui/src/utils/device.ts diff --git a/.github/instructions/lowcode-reference.instructions.md b/.github/instructions/lowcode-reference.instructions.md index 3a878ff2..78086d32 100644 --- a/.github/instructions/lowcode-reference.instructions.md +++ b/.github/instructions/lowcode-reference.instructions.md @@ -774,7 +774,7 @@ sessizce yok sayılır: | --- | --- | --- | | `GridBoxOptionsDto` | `dxGridBox` | `Columns[]` (ilk kolon `Key` ile aynı adda olmalı), `SelectionMode` (`"single"`), `FilterRowVisible` (`false`), `AcceptCustomValue` (`false`), `ShowClearButton` (`true`), `Height` (`250`), `Width` (`500`) | | `TagBoxOptionsDto` | `dxTagBox` | `ShowSelectionControls` (`true`), `MaxDisplayedTags` (`3`), `ShowMultiTagOnly` (`false`), `ApplyValueMode` (`"useButtons"`), `SearchEnabled` (`true`), `AcceptCustomValue` (`false`), `ShowClearButton` (`true`) — değer tek kolonda `\|` ile saklanır, bkz. §4.3 | -| `ImageUploadOptionsDto` | `dxImageUpload` · `dxImageViewer` | `UploadUrl`, `Accept`, `Multiple`, `MaxFileSize` (bayt), `Width`, `Height` — hepsi nullable, varsayılanı yok | +| `ImageUploadOptionsDto` | `dxImageUpload` · `dxImageViewer` | `UploadUrl`, `Accept`, `Multiple`, `MaxFileSize` (bayt), `Width`, `Height`, `Capture` (`environment`/`user` — mobilde dosya seçici yerine **yalnızca** kamerayı açar; boş bırakılırsa dokunmatik cihazda dosya seçicinin yanına ayrı bir kamera butonu gelir) — hepsi nullable, varsayılanı yok | Çözüm **yalnızca alanın `EditorType2` değeri eşleşiyorsa** yapılır: `dxTextBox` bir alana `columns` yazmak `GridBoxOptions`'ı doldurmaz. Ayrıca `ListFormField.EditorOptions`'ın ham @@ -1017,6 +1017,7 @@ alır): | `Knob` | `value`/`defaultValue` (`0`), `min`/`max` (`0/100`), `step` (`1`), `size` (`100`), `strokeWidth` (`14`), `valueColor`/`rangeColor`/`textColor`, `valueTemplate` (`'{value}'`), `showValue` (`true`), `readOnly`, `disabled`, `name` | | `Rate` | `count` (`5`), `value`/`defaultValue`, `allowHalf` (`false`), `allowClear` (`true`), `color` (`amber`), `size` (`20`), `gap` (`4`), `tooltips[]`, `disabled`, `readOnly` | | `Upload` | `accept`, `multiple`, `draggable` (`false`), `uploadLimit`, `showList` (`true`), `tip`, `fileList[]`, `disabled` — dosyanın **gerçek** yüklenmesi `beforeUpload`/`onChange` script'inde `api.post` ile yapılır | +| `BarcodeScanField` | `label`, `name` (`'barcode'`), `placeholder`, `buttonText`, `continuous` (`false` — açıkken diyalog okumadan sonra kapanmaz), `readOnly` (`true`), `className`; **olay**: `onScan(event)` — `event` okunan kodun metin hâli (Event workspace'te Input'un `onChange`'i gibi düzenlenir). Kamerayı `BarcodeDetector` (Android/Chromium) ya da lazy `zxing-wasm` (iOS/Firefox) ile okur — 1D + QR. **HTTPS şart**; iOS'ta yüklü PWA içinde yalnız 16.4+ çalışır, bileşen aksini kullanıcıya söyler | **Gösterim/geri bildirim bileşenleri:** diff --git a/.github/instructions/lowcode.instructions.md b/.github/instructions/lowcode.instructions.md index 4c1a0f41..871fd2b5 100644 --- a/.github/instructions/lowcode.instructions.md +++ b/.github/instructions/lowcode.instructions.md @@ -806,7 +806,7 @@ anahtarları listeler; ortak anahtarlar (§4.3.3) her editörde geçerlidir. | `dxSlider` | Tek değerli kaydırıcı | Sayı | `min`, `max`, `tooltip.showMode` | | `dxRangeSlider` | Aralık kaydırıcı | Sayı | `min`, `max`, `showRange` | | `dxHtmlEditor` | Zengin metin | String (16) | `toolbar.items`, `toolbar.multiline`, `valueType` (`html`/`markdown`), `mediaResizing.enabled`, `imageUpload.uploadUrl`, `imageUpload.fileUploadMode` | -| `dxImageUpload` | Görsel yükleme (platform) | String (16) | `uploadUrl`\*, `accept`\*, `multiple`\*, `maxFileSize`\*, `width`\*, `height`\* | +| `dxImageUpload` | Görsel yükleme (platform) | String (16) | `uploadUrl`\*, `accept`\*, `capture`\*, `multiple`\*, `maxFileSize`\*, `width`\*, `height`\* | | `dxImageViewer` | Görsel gösterimi (platform) | String (16) | `width`, `height` | `\*` ile işaretli anahtarlar backend'de **tipli DTO'ya** çözülür: @@ -1537,6 +1537,12 @@ Kurallar: her diyalog `open` + `onDialogClose` prop'larını alır. Kendi ekranını açmak istiyorsan **Adres** modunu kullan (custom component route'u ya da `/admin/list/{ListFormCode}`). +- `DialogName: "BarcodeScanDialog"` **toolbar butonuna özeldir** (`ButtonPosition: 1`) ve sürekli + barkod taramasını açar: okunan her kod listeye yeni bir satır olarak eklenir. + `DialogParameters`'ta `field` zorunludur (barkodun yazılacağı kolon); kalan anahtarlar her + satıra sabit değer olarak yazılır (`{"field":"Barcode","WarehouseId":"1"}`). Satır ekleme + ekranın kendi `InsertServiceAddress` ucundan geçer — yetki ve kiracı izolasyonu normal satır + eklemeyle aynıdır. Kamera HTTPS ister. - Onay akışının kendi butonları (Onayla/Reddet) `WorkflowJson` üzerinden **otomatik** gelir; onları `CommandColumnJson` ile tekrar tanımlama. @@ -1719,6 +1725,7 @@ bağlıdır. | interaction | `ask` | message | `if (!ask('Emin misiniz?')) return` — vazgeçilirse alan eski değerine döner ve script durur | | interaction | `openUrl` | url, target? | `openUrl('/report?id={Id}')` · `_self` verilirse ikinci argüman eklenir | | integration | `apiToField` | target, url, path? | `set('T', await api('/api/x/{value}', 'data.name'))` | +| integration | `location` | lat, lng, accuracy? | `await geoTo('Enlem', 'Boylam')` — cihaz konumunu alanlara yazar; HTTPS ve kullanıcı izni ister | | integration | `custom` | code | Satırın kendisi. Son çare; sondaki `;` atılır | `sum` ve `clear` çoklu alan alır (`fields` listesi); diğerlerinde her parametre tek alandır. @@ -1791,6 +1798,7 @@ Script `AsyncFunction` ile derlenir, aşağıdaki adlar parametre olarak enjekte | `revert()` | Alanı değişiklik öncesi değerine döndürür (editöre de yazar) | | `openUrl('/adres', '_blank')` | Yeni sekme/pencere | | `await api(url, 'data.name', options?)` | Ham `fetch`. **Kimlik doğrulama başlığı eklemez**; korumalı bir uç için Custom Endpoint tarafında erişim açık olmalıdır | +| `await geoTo('Lat', 'Lng', 'Acc'?)` | Cihaz konumunu alanlara yazar. İzin yoksa/okunamazsa alanlara **dokunmaz** ve uyarı gösterir, `false` döner | | `$` | Tüm yardımcıların tek nesne hâli | Eski (uzun) script'ler bozulmasın diye geriye dönük uyum vardır: script içinde yardımcı adlarıyla @@ -1841,7 +1849,7 @@ if (isReady) { | copy / setValue / clear | `Copy(sourcePath, target)` · `SetValue(target, text)` · `Clear("Il", "Ilce")` | | readOnly | `ReadOnly(target, invert)` | | notify / ask / openUrl | `Notify(message)` · `Ask(message)` · `OpenUrl(url, target)` | -| apiToField / custom | `ApiToField(target, url, responsePath)` · `Custom(code)` | +| apiToField / location / custom | `ApiToField(target, url, responsePath)` · `Location(latField, lngField, accuracyField)` · `Custom(code)` | Akıcı ekler: `.When(...)` (hepsi sağlanmalı), `.WhenAny(...)` (biri yeterli), `.OnChange()` / `.OnOpen()` / `.OnOpenAndChange()` / `.WithTrigger(EditorScriptTrigger.X)`, @@ -1938,7 +1946,7 @@ Dizi elemanları yaprak sayılmaz; `toolbar.items` tek bir `json` alanıdır. | Açılır liste | `searchEnabled`\*, `searchMode`, `searchExpr`, `searchTimeout`, `minSearchLength`, `showDataBeforeSearch`, `acceptCustomValue`\*, `noDataText`, `deferRendering`, `wrapItemText`, `dropDownOptions.width/height/hideOnOutsideClick` | `dxSelectBox`, `dxLookup`, `dxTagBox`, `dxDropDownBox`, `dxGridBox`, `dxAutocomplete` | | TagBox | `showSelectionControls`\*, `maxDisplayedTags`\*, `showMultiTagOnly`\*, `applyValueMode`\*, `multiline`, `hideSelectedItems` | `dxTagBox` | | GridBox | `columns`\*, `selectionMode`\*, `filterRowVisible`\* | `dxGridBox` | -| Görsel | `uploadUrl`\*, `fileFieldName`, `accept`\*, `multiple`\*, `maxFileSize`\* | `dxImageUpload`, `dxImageViewer` | +| Görsel | `uploadUrl`\*, `fileFieldName`, `accept`\*, `capture`\*, `multiple`\*, `maxFileSize`\* | `dxImageUpload`, `dxImageViewer` | | Seçim | `text` (`dxCheckBox`), `switchedOnText`/`switchedOffText` (`dxSwitch`), `layout` (`dxRadioGroup`), `editAlphaChannel`/`keyStep` (`dxColorBox`) | ilgili editör | | Kaydırıcı | `min`, `max`, `step`, `tooltip.enabled`, `tooltip.showMode`, `showRange` | `dxSlider`, `dxRangeSlider` | | HTML | `valueType`, `toolbar.multiline`, `toolbar.items`, `mediaResizing.enabled`, `imageUpload.fileUploadMode`, `imageUpload.uploadUrl` | `dxHtmlEditor` | @@ -1966,6 +1974,8 @@ 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 | `toolbar.items` (tam liste), `toolbar.multiline`, `mediaResizing`, `imageUpload` | | 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. | +| 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.5 Seeder tarafı (C#) diff --git a/README.md b/README.md index b46e1a1f..93ab7ec9 100644 --- a/README.md +++ b/README.md @@ -443,7 +443,7 @@ buradaki karşılığı da güncellenmelidir. | Tip | Kullanım | | --- | --- | -| `EditorScript` | `Multiply`, `Subtract`, `Percent`, `Sum`, `Formula`, `Today`, `Days`, `Hours`, `Copy`, `SetValue`, `Clear`, `ReadOnly`, `Notify`, `Ask`, `OpenUrl`, `ApiToField`, `Custom` → `EditorScript.Build(rules)` | +| `EditorScript` | `Multiply`, `Subtract`, `Percent`, `Sum`, `Formula`, `Today`, `Days`, `Hours`, `Copy`, `SetValue`, `Clear`, `ReadOnly`, `Notify`, `Ask`, `OpenUrl`, `ApiToField`, `Location`, `Custom` → `EditorScript.Build(rules)` | | `EditorScriptRule` | `.When(...)` / `.WhenAny(...)` koşulları, `.OnChange()` / `.OnOpen()` / `.OnOpenAndChange()` tetikleyicisi; `string`'e örtük dönüşür | | `EditorScriptCondition` | `Is`, `IsNot`, `Contains`, `IsEmpty`, `IsNotEmpty`, `GreaterThan`, `LessThan`, `IsTrue`, `IsFalse`, `Always` | | `EditorOptions` | Hazır başlangıçlar: `Disabled()`, `ReadOnly()`, `ShowClearButton()`, `Multiline(h)`, `Number(precision)`, `Percent()`, `Date()`, `DateTime()`, `Time(interval)`, `Phone()`, `Slider()`, `ImageUpload()`, `Html(h)` | @@ -1019,8 +1019,8 @@ dosyası yoktur; liste her `npm run build` / `npm start` ile tazelenir. **Katalog düğümleri.** Kanvasın doğrudan çizdiği, derlenen bileşen listesinde yer almayan bileşenler tek bir sözlükte durur (`RUNTIME_NODE_COMPONENTS`): kanvas onu `VisualCanvas` içinde, üretilen kod ise runtime kapsamı üzerinden kullanır — iki taraf aynı kaynaktan beslendiği için -tasarım ve çalışma zamanı ayrışamaz. Sözlükte `PlatformIcon` ve public site parçaları -(`components/publicSite`) bulunur. Yeni bir düğüm eklerken: bileşen `components/publicSite` (ya da +tasarım ve çalışma zamanı ayrışamaz. Sözlükte `PlatformIcon`, `BarcodeScanField` +(`components/shared/BarcodeScanner`) ve public site parçaları (`components/publicSite`) bulunur. Yeni bir düğüm eklerken: bileşen `components/publicSite` (ya da uygun klasör) → `runtimeNodes.ts` sözlüğü → `catalog.ts` tanımı (`kind: 'custom'`) → `LanguagesData.json` açıklama anahtarı. diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/ImageUploadOptionsDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/ImageUploadOptionsDto.cs index 17878adf..9c2bdae0 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/ImageUploadOptionsDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/ImageUploadOptionsDto.cs @@ -8,4 +8,10 @@ public class ImageUploadOptionsDto public long? MaxFileSize { get; set; } public int? Width { get; set; } public int? Height { get; set; } + + /// + /// Mobil cihazda dosya secici yerine dogrudan kamerayi acar: + /// "environment" arka, "user" on kamera. Bos birakilirsa dosya secici acilir. + /// + public string Capture { get; set; } } diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json index de7edf5b..dec99ce7 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json @@ -28895,6 +28895,198 @@ "key": "App.Listform.ListformField.OrderId", "en": "Order", "tr": "Sipariş" + }, + { + "resourceName": "Platform", + "key": "App.BarcodeScanner.Title", + "en": "Scan barcode", + "tr": "Barkod okut" + }, + { + "resourceName": "Platform", + "key": "App.BarcodeScanner.Starting", + "en": "Starting the camera...", + "tr": "Kamera açılıyor..." + }, + { + "resourceName": "Platform", + "key": "App.BarcodeScanner.Aim", + "en": "Hold the barcode inside the frame.", + "tr": "Barkodu çerçevenin içinde tutun." + }, + { + "resourceName": "Platform", + "key": "App.BarcodeScanner.PermissionDenied", + "en": "Camera permission was denied. Allow camera access for this site and try again.", + "tr": "Kamera izni verilmedi. Bu site için kamera erişimine izin verip tekrar deneyin." + }, + { + "resourceName": "Platform", + "key": "App.BarcodeScanner.InsecureContext", + "en": "The camera is only available over HTTPS.", + "tr": "Kamera yalnızca HTTPS üzerinden kullanılabilir." + }, + { + "resourceName": "Platform", + "key": "App.BarcodeScanner.StandaloneIos", + "en": "On this iOS version the camera does not work in the installed app. Open the page in Safari.", + "tr": "Bu iOS sürümünde kamera yüklü uygulama içinde çalışmıyor. Sayfayı Safari'de açın." + }, + { + "resourceName": "Platform", + "key": "App.BarcodeScanner.CameraError", + "en": "The camera could not be started.", + "tr": "Kamera başlatılamadı." + }, + { + "resourceName": "Platform", + "key": "App.BarcodeScanner.SaveFailed", + "en": "The scanned code could not be saved.", + "tr": "Okunan kod kaydedilemedi." + }, + { + "resourceName": "Platform", + "key": "App.BarcodeScanner.ScannedCount", + "en": "Scanned", + "tr": "Okunan" + }, + { + "resourceName": "Platform", + "key": "App.BarcodeScanner.NotConfigured", + "en": "The barcode button is not configured: a target field and an insert address are required.", + "tr": "Barkod butonu yapılandırılmamış: hedef alan ve ekleme adresi gerekli." + }, + { + "resourceName": "Platform", + "key": "App.BarcodeScanner.Placeholder", + "en": "Scan a barcode", + "tr": "Barkod okutun" + }, + { + "resourceName": "Platform", + "key": "App.BarcodeScanner.Scan", + "en": "Scan", + "tr": "Okut" + }, + { + "resourceName": "Platform", + "key": "App.PresetBarcodeScan.BarcodeScanLabel", + "en": "Barcode scan button", + "tr": "Barkod okutma butonu" + }, + { + "resourceName": "Platform", + "key": "App.PresetBarcodeScan.BarcodeScanDescription", + "en": "Adds a camera button next to the text box; the scanned code is written into the field.", + "tr": "Metin kutusunun yanına kamera butonu ekler; okunan kod alana yazılır." + }, + { + "resourceName": "Platform", + "key": "App.CatalogBarcodeScanField.BarcodeScanFieldDescription", + "en": "Text field with a camera button that reads barcodes and QR codes.", + "tr": "Barkod ve QR okuyan kamera butonlu metin alanı." + }, + { + "resourceName": "Platform", + "key": "App.Geolocation.Unsupported", + "en": "This device does not provide location information.", + "tr": "Bu cihaz konum bilgisi vermiyor." + }, + { + "resourceName": "Platform", + "key": "App.Geolocation.Failed", + "en": "Location could not be read. Allow location access and try again.", + "tr": "Konum okunamadı. Konum erişimine izin verip tekrar deneyin." + }, + { + "resourceName": "Platform", + "key": "App.ScriptBuilderLocation.LocationLabel", + "en": "Write device location", + "tr": "Cihaz konumunu yaz" + }, + { + "resourceName": "Platform", + "key": "App.ScriptBuilderLocation.LocationSummary", + "en": "Reads the device location once and writes it into the latitude/longitude fields. Requires HTTPS and the user's permission.", + "tr": "Cihaz konumunu tek seferde okur ve enlem/boylam alanlarına yazar. HTTPS ve kullanıcı izni gerekir." + }, + { + "resourceName": "Platform", + "key": "App.ScriptBuilderLocation.LatLabel", + "en": "Latitude field", + "tr": "Enlem alanı" + }, + { + "resourceName": "Platform", + "key": "App.ScriptBuilderLocation.LngLabel", + "en": "Longitude field", + "tr": "Boylam alanı" + }, + { + "resourceName": "Platform", + "key": "App.ScriptBuilderLocation.AccuracyLabel", + "en": "Accuracy field (m)", + "tr": "Doğruluk alanı (m)" + }, + { + "resourceName": "Platform", + "key": "App.ScriptBuilderLocation.AccuracyHelp", + "en": "Optional. The accuracy radius reported by the device, in metres.", + "tr": "İsteğe bağlı. Cihazın bildirdiği doğruluk yarıçapı, metre." + }, + { + "resourceName": "Platform", + "key": "App.ScriptBuilderDescribe.Location", + "en": "Writes the device location into {lat} and {lng}.", + "tr": "Cihaz konumunu {lat} ve {lng} alanlarına yazar." + }, + { + "resourceName": "Platform", + "key": "App.SpecCapture.Help", + "en": "On mobile, opens the camera directly instead of the file picker. Leave empty for the file picker.", + "tr": "Mobilde dosya seçici yerine doğrudan kamerayı açar. Boş bırakılırsa dosya seçici açılır." + }, + { + "resourceName": "Platform", + "key": "App.SpecCapture.Environment", + "en": "Rear camera", + "tr": "Arka kamera" + }, + { + "resourceName": "Platform", + "key": "App.SpecCapture.User", + "en": "Front camera", + "tr": "Ön kamera" + }, + { + "resourceName": "Platform", + "key": "App.PresetPhotoCapture.PhotoCaptureLabel", + "en": "Take photo with camera", + "tr": "Kamerayla foto çek" + }, + { + "resourceName": "Platform", + "key": "App.PresetPhotoCapture.PhotoCaptureDescription", + "en": "accept: image/* + capture: environment — on mobile the rear camera opens directly.", + "tr": "accept: image/* + capture: environment — mobilde doğrudan arka kamera açılır." + }, + { + "resourceName": "Platform", + "key": "App.CatalogBarcodeScanField.OnScanDescription", + "en": "Runs on every successful scan; event is the scanned code as text.", + "tr": "Her başarılı okumada çalışır; event okunan kodun metin halidir." + }, + { + "resourceName": "Platform", + "key": "App.ImageUpload.Upload", + "en": "Upload image", + "tr": "Görsel yükle" + }, + { + "resourceName": "Platform", + "key": "App.ImageUpload.TakePhoto", + "en": "Take photo with camera", + "tr": "Kamerayla fotoğraf çek" } ] } diff --git a/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScript.cs b/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScript.cs index ccef837f..dfd98da1 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScript.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScript.cs @@ -136,6 +136,14 @@ public static class EditorScript public static EditorScriptRule ApiToField(string target, string url, string responsePath = null) => new EditorScriptRule("apiToField").Set("target", target).Set("url", url).Set("path", responsePath); + /// + /// Cihazin konumunu alanlara yazar. accuracyField bos birakilirsa dogruluk + /// yarcapi yazilmaz. Kamera gibi konum da HTTPS ister. + /// + public static EditorScriptRule Location(string latField, string lngField, string accuracyField = null) + => new EditorScriptRule("location") + .Set("lat", latField).Set("lng", lngField).Set("accuracy", accuracyField); + /// Serbest JS satiri. Runtime yardimcilari (get/set/num/...) burada da kullanilabilir. public static EditorScriptRule Custom(string code) => new EditorScriptRule("custom").Set("code", code); diff --git a/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScriptRule.cs b/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScriptRule.cs index 9815c538..2e490b5e 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScriptRule.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/Editors/EditorScriptRule.cs @@ -29,6 +29,7 @@ public sealed class EditorScriptRule ["ask"] = new[] { "message" }, ["openUrl"] = new[] { "url" }, ["apiToField"] = new[] { "target", "url" }, + ["location"] = new[] { "lat", "lng" }, ["custom"] = new[] { "code" } }; @@ -59,6 +60,7 @@ public sealed class EditorScriptRule ["ask"] = EditorScriptTrigger.Change, ["openUrl"] = EditorScriptTrigger.Change, ["apiToField"] = EditorScriptTrigger.Change, + ["location"] = EditorScriptTrigger.Change, ["custom"] = EditorScriptTrigger.Change }; @@ -382,6 +384,10 @@ public sealed class EditorScriptRule return "set(" + q(Param("target")) + ", await api(" + q(Param("url")) + (Param("path").Length > 0 ? ", " + q(Param("path")) : string.Empty) + "))"; + case "location": + return "await geoTo(" + q(Param("lat")) + ", " + q(Param("lng")) + + (Param("accuracy").Length > 0 ? ", " + q(Param("accuracy")) : string.Empty) + ")"; + case "custom": return Param("code").Trim().TrimEnd(';'); diff --git a/configs/seeds/host/data/App.DeveloperKit.CustomComponents.json b/configs/seeds/host/data/App.DeveloperKit.CustomComponents.json index 3eee97a4..e72b046b 100644 --- a/configs/seeds/host/data/App.DeveloperKit.CustomComponents.json +++ b/configs/seeds/host/data/App.DeveloperKit.CustomComponents.json @@ -1,7 +1,7 @@ { "ListFormCode": "App.DeveloperKit.CustomComponents", "KeyFieldName": "Id", - "GeneratedAt": "2026-09-08T13:11:35.4554636Z", + "GeneratedAt": "2026-09-21T14:46:03.8784173Z", "Order": 0, "Rows": [ { @@ -47,92 +47,10 @@ "RoutePath": "/admin/roles", "Description": "", "IsActive": true, - "Dependencies": [], - "Code": "/*__SOZSOFT_VISUAL_DESIGNER__%7B%22version%22%3A1%2C%22sourceMode%22%3A%22visual%22%2C%22nodes%22%3A%5B%7B%22id%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22type%22%3A%22Form%22%2C%22kind%22%3A%22layout%22%2C%22props%22%3A%7B%22selectEndpoint%22%3A%22source_5w_5ibpv%22%2C%22insertEndpoint%22%3A%22source_h3_484qa%22%2C%22updateEndpoint%22%3A%22source_gb_d9cvs%22%2C%22deleteEndpoint%22%3A%22source_0t_avok9%22%2C%22keyFieldName%22%3A%22id%22%2C%22collectionPath%22%3A%22%22%2C%22keySource%22%3A%22query%22%2C%22keyParamName%22%3A%22id%22%2C%22previewKeyValue%22%3A%22%22%2C%22autoLoad%22%3Atrue%2C%22showToolbar%22%3Atrue%2C%22gap%22%3A16%2C%22className%22%3A%22%22%2C%22title%22%3A%22%3A%3AAbpIdentity.Roles%22%2C%22columnCount%22%3A2%2C%22selectPermission%22%3A%22%22%2C%22insertPermission%22%3A%22%22%7D%2C%22events%22%3A%7B%22onLoad%22%3A%22%22%2C%22onRecordChange%22%3A%22%22%2C%22onFieldChange%22%3A%22%22%2C%22onNewRecord%22%3A%22%22%2C%22onModeChange%22%3A%22%22%2C%22onBeforeSave%22%3A%22%22%2C%22onAfterSave%22%3A%22%22%2C%22onBeforeDelete%22%3A%22%22%2C%22onAfterDelete%22%3A%22%22%2C%22onError%22%3A%22%22%7D%2C%22bindings%22%3A%7B%7D%2C%22children%22%3A%5B%7B%22id%22%3A%22cmp_msyfpfia_ifhdq%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22%3A%3AApp.Platform.Name%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%2C%22sqlDefaultValue%22%3A%22%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22Name%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input1%22%7D%2C%7B%22id%22%3A%22cmp_msyjl707_crcn7%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Normalized%20Name%20%3A%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22NormalizedName%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input2%22%7D%2C%7B%22id%22%3A%22cmp_msyjdodn_u3o74%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Concurrent%20Stamp%20%3A%20%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22ConcurrencyStamp%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input3%22%7D%2C%7B%22id%22%3A%22cmp_msyje9ys_2c08a%22%2C%22type%22%3A%22Checkbox%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22checked%22%3Afalse%2C%22color%22%3A%22blue-500%22%2C%22disabled%22%3Afalse%2C%22labelRef%22%3A%22%22%2C%22name%22%3A%22%22%2C%22readOnly%22%3Afalse%2C%22value%22%3A%22%22%2C%22field%22%3Anull%2C%22className%22%3A%22%22%2C%22children%22%3A%22Is%20Default%22%2C%22style%22%3A%7B%7D%2C%22sqlDefaultValue%22%3A%22true%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22checked%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22IsDefault%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22checkbox1%22%7D%2C%7B%22id%22%3A%22cmp_msyjebu2_27tqi%22%2C%22type%22%3A%22Checkbox%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22checked%22%3Afalse%2C%22color%22%3A%22blue-500%22%2C%22disabled%22%3Afalse%2C%22labelRef%22%3A%22%22%2C%22name%22%3A%22%22%2C%22readOnly%22%3Afalse%2C%22value%22%3A%22%22%2C%22field%22%3Anull%2C%22className%22%3A%22%22%2C%22children%22%3A%22Is%20Static%22%2C%22style%22%3A%7B%7D%2C%22sqlDefaultValue%22%3A%22true%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22checked%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22IsStatic%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22checkbox2%22%7D%2C%7B%22id%22%3A%22cmp_msyjmetn_gojol%22%2C%22type%22%3A%22Checkbox%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22checked%22%3Afalse%2C%22color%22%3A%22blue-500%22%2C%22disabled%22%3Afalse%2C%22labelRef%22%3A%22%22%2C%22name%22%3A%22%22%2C%22readOnly%22%3Afalse%2C%22value%22%3A%22%22%2C%22field%22%3Anull%2C%22className%22%3A%22%22%2C%22children%22%3A%22Is%20Public%22%2C%22style%22%3A%7B%7D%2C%22sqlDefaultValue%22%3A%22false%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22checked%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22IsPublic%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22checkbox3%22%7D%2C%7B%22id%22%3A%22cmp_msyjo0g8_oathk%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Entity%20Version%20%3A%20%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22number%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%2C%22sqlDefaultValue%22%3A%223%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22EntityVersion%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input4%22%7D%2C%7B%22id%22%3A%22cmp_msyjplfq_py8lz%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Extra%20Properties%20%3A%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%2C%22sqlDefaultValue%22%3A%22%7B%7D%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22ExtraProperties%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input5%22%7D%5D%2C%22ref%22%3A%22form1%22%7D%2C%7B%22id%22%3A%22cmp_mt023r5u_rb2jv%22%2C%22type%22%3A%22Grid%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22hoverable%22%3Atrue%2C%22overflow%22%3Atrue%2C%22borderlessRow%22%3Afalse%2C%22compact%22%3Afalse%2C%22cols%22%3A%223%22%2C%22rows%22%3A%222%22%2C%22gap%22%3A%224%22%2C%22colGap%22%3A%224%22%2C%22rowGap%22%3A%224%22%2C%22responsive%22%3Atrue%2C%22smCols%22%3A%221%22%2C%22mdCols%22%3A%222%22%2C%22lgCols%22%3A%223%22%2C%22xlCols%22%3A%224%22%2C%22autoFit%22%3Afalse%2C%22minColWidth%22%3A%22200px%22%2C%22height%22%3A%22auto%22%2C%22className%22%3A%22mt-4%22%2C%22items%22%3A%5B%7B%22id%22%3A%22grid-item-1%22%2C%22content%22%3A%22Grid%20Item%201%22%2C%22colSpan%22%3A1%2C%22rowSpan%22%3A1%2C%22colStart%22%3A1%2C%22rowStart%22%3A1%2C%22className%22%3A%22bg-blue-100%20p-4%20border%20rounded%22%7D%2C%7B%22id%22%3A%22grid-item-2%22%2C%22content%22%3A%22Grid%20Item%202%22%2C%22colSpan%22%3A1%2C%22rowSpan%22%3A1%2C%22colStart%22%3A2%2C%22rowStart%22%3A1%2C%22className%22%3A%22bg-green-100%20p-4%20border%20rounded%22%7D%2C%7B%22id%22%3A%22grid-item-3%22%2C%22content%22%3A%22Grid%20Item%203%22%2C%22colSpan%22%3A1%2C%22rowSpan%22%3A1%2C%22colStart%22%3A3%2C%22rowStart%22%3A1%2C%22className%22%3A%22bg-yellow-100%20p-4%20border%20rounded%22%7D%2C%7B%22id%22%3A%22grid-item-4%22%2C%22content%22%3A%22Grid%20Item%204%22%2C%22colSpan%22%3A2%2C%22rowSpan%22%3A1%2C%22colStart%22%3A1%2C%22rowStart%22%3A2%2C%22className%22%3A%22bg-purple-100%20p-4%20border%20rounded%22%7D%2C%7B%22id%22%3A%22grid-item-5%22%2C%22content%22%3A%22Grid%20Item%205%22%2C%22colSpan%22%3A1%2C%22rowSpan%22%3A1%2C%22colStart%22%3A3%2C%22rowStart%22%3A2%2C%22className%22%3A%22bg-red-100%20p-4%20border%20rounded%22%7D%5D%2C%22dataColumns%22%3A%5B%22UserId%22%5D%2C%22columnLookups%22%3A%7B%22UserId%22%3A%7B%22sourceId%22%3A%22source_gq_0q1dz%22%2C%22path%22%3A%22%22%2C%22valueField%22%3A%22Id%22%2C%22textField%22%3A%22UserName%22%7D%7D%2C%22columnCaptions%22%3A%7B%7D%7D%2C%22events%22%3A%7B%7D%2C%22bindings%22%3A%7B%22items%22%3A%7B%22sourceId%22%3A%22source_tq_veb4m%22%2C%22path%22%3A%22%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22grid1%22%7D%2C%7B%22id%22%3A%22cmp_mt0288i3_ebpj5%22%2C%22type%22%3A%22ListView%22%2C%22kind%22%3A%22platform%22%2C%22props%22%3A%7B%22listFormCode%22%3A%22AbpIdentity.Roles%22%2C%22height%22%3A%22520px%22%2C%22filters%22%3A%5B%7B%22id%22%3A%22filter_y9_tr73w%22%2C%22field%22%3A%22Id%22%2C%22operator%22%3A%22eq%22%2C%22source%22%3A%22record%22%2C%22value%22%3A%22form1.Id%22%2C%22previewValue%22%3A%22%22%2C%22required%22%3Atrue%7D%5D%2C%22className%22%3A%22mt-4%22%7D%2C%22events%22%3A%7B%7D%2C%22bindings%22%3A%7B%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22listView1%22%7D%5D%2C%22canvas%22%3A%7B%22width%22%3A%22responsive%22%7D%2C%22lifecycle%22%3A%7B%22onMount%22%3A%22%22%7D%2C%22dataSources%22%3A%5B%7B%22id%22%3A%22source_5w_5ibpv%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20GetList%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_i7_msox4%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20GetById%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_0t_avok9%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20Delete%22%2C%22method%22%3A%22DELETE%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_h3_484qa%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20Create%22%2C%22method%22%3A%22POST%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_gb_d9cvs%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20Update%22%2C%22method%22%3A%22PUT%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_tq_veb4m%22%2C%22name%22%3A%22AbpUserRoles%20%C2%B7%20GetList%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpUserRoles%22%2C%22responsePath%22%3A%22%22%2C%22filters%22%3A%5B%7B%22id%22%3A%22filter_sw_86e0q%22%2C%22field%22%3A%22RoleId%22%2C%22operator%22%3A%22eq%22%2C%22source%22%3A%22record%22%2C%22value%22%3A%22form1.id%22%2C%22previewValue%22%3A%22%22%2C%22required%22%3Atrue%7D%5D%7D%2C%7B%22id%22%3A%22source_d2_bri5s%22%2C%22name%22%3A%22AbpUserRoles%20%C2%B7%20GetById%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpUserRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_gq_0q1dz%22%2C%22name%22%3A%22AbpUsers%20%C2%B7%20GetList%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpUsers%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_u8_nu1lu%22%2C%22name%22%3A%22AbpUsers%20%C2%B7%20GetById%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpUsers%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%5D%2C%22permissionCode%22%3A%22%22%7D__*/\nconst RoleComponent = () => {\n const getByPath = (value, path) => {\n if (!path) return value\n const readPath = (target, targetPath) => targetPath.split('.').filter(Boolean).reduce((current, key) => current?.[key], target)\n const directValue = readPath(value, path)\n if (directValue !== undefined) return directValue\n if (path.startsWith('result.')) return readPath(value, path.slice('result.'.length))\n return value?.result === undefined ? undefined : readPath(value.result, path)\n }\n\n const resolveLookupText = (lookup, value) => {\n if (!lookup || value === null || value === undefined || value === \"\") return undefined\n const rows = getByPath(lookup.rows, lookup.path)\n if (!Array.isArray(rows)) return undefined\n const key = String(value)\n const row = rows.find((item) => item && typeof item === \"object\" && String(getByPath(item, lookup.value) ?? \"\") === key)\n if (!row) return undefined\n const text = getByPath(row, lookup.text)\n return text === null || text === undefined || text === \"\" ? undefined : text\n }\n\n const localeCulture = () => {\n const injected = typeof getCulture === \"function\" ? getCulture() : \"\"\n if (injected) return injected\n if (typeof document !== \"undefined\" && document.documentElement.lang) return document.documentElement.lang\n return undefined\n }\n // Patterns come from the culture itself; a fixed day/month/year order would\n // print the same layout in every language.\n const localeDateOptions = { dateStyle: \"short\" }\n const localeTimeOptions = { dateStyle: \"short\", timeStyle: \"short\" }\n const localeIsoPattern = /^(\\d{4})-(\\d{2})-(\\d{2})(?:[T ](\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d+)?)?(Z|[+-]\\d{2}:?\\d{2})?)?$/\n const formatLocaleValue = (value) => {\n if (value === null || value === undefined) return \"\"\n const culture = localeCulture()\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) return String(value)\n // Grouping and the decimal separator are the culture's decision; only the\n // precision guard stays, so a long decimal is not rounded away.\n return new Intl.NumberFormat(culture, { maximumFractionDigits: 20 }).format(value)\n }\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? \"\" : new Intl.DateTimeFormat(culture, localeTimeOptions).format(value)\n }\n if (typeof value !== \"string\") return String(value)\n const parts = localeIsoPattern.exec(value.trim())\n if (!parts) return value\n const parsed = new Date(value)\n if (Number.isNaN(parsed.getTime())) return value\n if (parts[4] === undefined) {\n const day = new Date(Number(parts[1]), Number(parts[2]) - 1, Number(parts[3]))\n return new Intl.DateTimeFormat(culture, localeDateOptions).format(day)\n }\n return new Intl.DateTimeFormat(culture, localeTimeOptions).format(parsed)\n }\n\n const readSqlField = (record, field) => {\n if (!record || typeof record !== \"object\" || Array.isArray(record)) return undefined\n if (field in record) return record[field]\n const matched = Object.keys(record).find((key) => key.toLowerCase() === String(field).toLowerCase())\n return matched === undefined ? undefined : record[matched]\n }\n const toSqlRows = (value, collectionPath) => {\n const source = collectionPath ? getByPath(value, collectionPath) : value\n const rows = Array.isArray(source)\n ? source\n : source && typeof source === \"object\"\n ? (Array.isArray(source.items) ? source.items : [source])\n : []\n return rows.filter((item) => item && typeof item === \"object\" && !Array.isArray(item))\n }\n const setSqlField = (record, path, value) => {\n const keys = String(path || \"\").split(\".\").filter(Boolean)\n if (!keys.length) return record\n const next = { ...(record || {}) }\n let target = next\n for (let index = 0; index < keys.length - 1; index += 1) {\n const child = target[keys[index]]\n target[keys[index]] = child && typeof child === \"object\" && !Array.isArray(child) ? { ...child } : {}\n target = target[keys[index]]\n }\n target[keys[keys.length - 1]] = value\n return next\n }\n const bindSqlUrl = (url, record, keyField) => {\n let keyBound = false\n const boundUrl = String(url).replace(/\\{([^}]+)\\}|(?<=\\/):([A-Za-z_][A-Za-z0-9_]*)/g, (match, braced, colon) => {\n const name = (braced || colon || \"\").trim()\n if (!name) return match\n const value = readSqlField(record, name) ?? readSqlField(record, keyField)\n if (value === undefined || value === null || value === \"\") return match\n if (name.toLowerCase() === String(keyField).toLowerCase()) keyBound = true\n return encodeURIComponent(String(value))\n })\n return { url: boundUrl, keyBound }\n }\n const appendQueryParam = (url, name, value) =>\n url + (url.includes(\"?\") ? \"&\" : \"?\") + encodeURIComponent(name) + \"=\" + encodeURIComponent(value)\n const hasSqlUrlParams = (url) => /\\{[^}]+\\}|(?<=\\/):[A-Za-z_][A-Za-z0-9_]*/.test(String(url))\n const readUrlKey = (source, name) => {\n if (typeof window === \"undefined\") return \"\"\n if (source === \"query\") return new URLSearchParams(window.location.search).get(name) ?? \"\"\n const segments = window.location.pathname.split(\"/\").filter(Boolean)\n return segments.length ? decodeURIComponent(segments[segments.length - 1]) : \"\"\n }\n const filterSqlRowsByKey = (rows, keyField, keyValue) => {\n if (!keyValue) return rows\n const matched = rows.filter((row) => String(readSqlField(row, keyField) ?? \"\") === String(keyValue))\n // No match means the endpoint already filtered server side, or the key column\n // is named differently — keeping the rows beats showing an empty form.\n return matched.length ? matched : rows\n }\n // Update payload: only the columns the user actually edited, plus the key.\n const toSqlChanges = (original, current, keyField) => {\n const changes = {}\n const isSame = (left, right) => JSON.stringify(left ?? null) === JSON.stringify(right ?? null)\n Object.keys(current || {}).forEach((column) => {\n if (!isSame(original ? original[column] : undefined, current[column])) changes[column] = current[column]\n })\n const keyColumn = Object.keys(current || {}).find((column) => column.toLowerCase() === String(keyField).toLowerCase())\n if (keyColumn !== undefined && changes[keyColumn] === undefined) changes[keyColumn] = current[keyColumn]\n return changes\n }\n const callSqlEndpoint = (target, record, keyField, payload) => {\n // The URL is always bound from the full record: a placeholder may reference a\n // column that the update payload does not carry.\n const bound = bindSqlUrl(target.url, record, keyField)\n const request = { url: bound.url, method: target.method }\n if (target.method === \"POST\" || target.method === \"PUT\") request.data = payload === undefined ? record : payload\n const keyValue = readSqlField(record, keyField)\n const hasKey = keyValue !== undefined && keyValue !== null && keyValue !== \"\"\n // The key still has to reach the endpoint when the URL carries no placeholder.\n if (!bound.keyBound && hasKey && target.method !== \"POST\") request.params = { [keyField]: keyValue }\n return apiService.fetchData(request)\n }\n // A new record starts at the top of the form: the first enabled editor takes the\n // cursor, so a Yeni click is immediately followed by typing.\n const focusFirstSqlField = (host) => {\n if (typeof window === \"undefined\") return\n window.requestAnimationFrame(() => {\n const field = host.current?.querySelector(\"input:not([type='hidden']):not([disabled]):not([readonly]), textarea:not([disabled]):not([readonly]), select:not([disabled])\")\n if (!field) return\n field.focus()\n if (typeof field.select === \"function\") field.select()\n })\n }\n // Defaults declared on the bound children; only a new record starts from them.\n const buildSqlDefaults = (fields) => fields.reduce((record, field) => setSqlField(record, field.path, resolveSqlDefault(field)), {})\n const resolveSqlDefault = (field) => {\n if (field.token !== \"today\" && field.token !== \"now\") return field.value\n const now = new Date()\n const pad = (value) => String(value).padStart(2, \"0\")\n const day = now.getFullYear() + \"-\" + pad(now.getMonth() + 1) + \"-\" + pad(now.getDate())\n if (field.token === \"today\" || field.dateOnly) return day\n return day + \"T\" + pad(now.getHours()) + \":\" + pad(now.getMinutes()) + \":\" + pad(now.getSeconds())\n }\n const toSqlErrorMessage = (error) => error?.response?.data?.error?.message || error?.response?.data?.message || error?.message || translate(\"::App.Platform.IslemBasarisiz\")\n\n // Value of a filter that reads the page URL: a query string parameter, or a\n // path segment — the one after a segment named like the parameter, otherwise\n // the last one, which is the /orders/{id} detail page shape.\n const readDesignerUrlValue = (kind, name) => {\n if (typeof window === \"undefined\") return \"\"\n if (kind === \"query\") return new URLSearchParams(window.location.search).get(name) ?? \"\"\n const segments = window.location.pathname.split(\"/\").filter(Boolean)\n if (!segments.length) return \"\"\n const index = name ? segments.findIndex((segment) => segment.toLowerCase() === String(name).toLowerCase()) : -1\n if (index >= 0 && index < segments.length - 1) return decodeURIComponent(segments[index + 1])\n return decodeURIComponent(segments[segments.length - 1])\n }\n // Filters are appended as query parameters: `Column` for an equality and\n // `Column.operator` for anything else, which is what the endpoint parses.\n // Returns \"\" when a required filter has no value yet — the caller must skip the\n // request instead of loading the unfiltered collection.\n const buildFilteredUrl = (url, filters) => {\n const parts = []\n for (const filter of filters || []) {\n const value = filter.value === undefined || filter.value === null ? \"\" : String(filter.value)\n if (!value) {\n if (filter.required) return \"\"\n continue\n }\n parts.push(encodeURIComponent(filter.param) + \"=\" + encodeURIComponent(value))\n }\n if (!parts.length) return url\n return url + (url.includes(\"?\") ? \"&\" : \"?\") + parts.join(\"&\")\n }\n\n const buildPlatformFilterParams = (filters) => {\n const expressions = []\n for (const filter of filters || []) {\n if (filter.kind === \"isnull\" || filter.kind === \"notnull\") {\n expressions.push([filter.field, filter.operator, null])\n continue\n }\n const value = filter.value === undefined || filter.value === null ? \"\" : String(filter.value)\n if (!value) {\n if (filter.required) return null\n continue\n }\n if (filter.kind === \"in\") {\n const entries = value.split(\",\").map((entry) => entry.trim()).filter(Boolean).map((entry) => [filter.field, \"=\", entry])\n if (!entries.length) continue\n expressions.push(entries.reduce((result, entry) => (result ? [result, \"or\", entry] : entry), null))\n continue\n }\n expressions.push([filter.field, filter.operator, value])\n }\n const params = new URLSearchParams()\n const merged = expressions.reduce((result, expression) => (result ? [result, \"and\", expression] : expression), null)\n if (merged) params.set(\"filter\", JSON.stringify(merged))\n return params\n }\n\n const [data_source_5w_5ibpv, setData_source_5w_5ibpv] = React.useState(null)\n const [data_source_i7_msox4, setData_source_i7_msox4] = React.useState(null)\n const [data_source_0t_avok9] = React.useState(null)\n const [data_source_h3_484qa] = React.useState(null)\n const [data_source_gb_d9cvs] = React.useState(null)\n const [data_source_tq_veb4m, setData_source_tq_veb4m] = React.useState(null)\n const [data_source_d2_bri5s, setData_source_d2_bri5s] = React.useState(null)\n const [data_source_gq_0q1dz, setData_source_gq_0q1dz] = React.useState(null)\n const [data_source_u8_nu1lu, setData_source_u8_nu1lu] = React.useState(null)\n\n const sqlselect_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles\", method: \"GET\", responsePath: \"\" }\n const sqlinsert_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles\", method: \"POST\", responsePath: \"\" }\n const sqlupdate_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles/{id}\", method: \"PUT\", responsePath: \"\" }\n const sqldelete_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles/{id}\", method: \"DELETE\", responsePath: \"\" }\n const sqlCanSelect_cmp_msyfmegt_jkkn3 = true\n const sqlCanInsert_cmp_msyfmegt_jkkn3 = true\n const sqlCanUpdate_cmp_msyfmegt_jkkn3 = true\n const sqlCanDelete_cmp_msyfmegt_jkkn3 = true\n const sqlKey_cmp_msyfmegt_jkkn3 = \"id\"\n const sqlKeyParam_cmp_msyfmegt_jkkn3 = \"id\"\n const [data_cmp_msyfmegt_jkkn3, setData_cmp_msyfmegt_jkkn3] = React.useState({})\n const [sqlOriginal_cmp_msyfmegt_jkkn3, setSqlOriginal_cmp_msyfmegt_jkkn3] = React.useState({})\n const [sqlMode_cmp_msyfmegt_jkkn3, setSqlMode_cmp_msyfmegt_jkkn3] = React.useState(\"edit\")\n const [sqlIndex_cmp_msyfmegt_jkkn3, setSqlIndex_cmp_msyfmegt_jkkn3] = React.useState(0)\n const [sqlBusy_cmp_msyfmegt_jkkn3, setSqlBusy_cmp_msyfmegt_jkkn3] = React.useState(false)\n const [sqlError_cmp_msyfmegt_jkkn3, setSqlError_cmp_msyfmegt_jkkn3] = React.useState(\"\")\n // Container element, so New can hand the cursor to the first field of the form.\n const sqlHost_cmp_msyfmegt_jkkn3 = React.useRef(null)\n // The Select key can come from the page URL, which is how a detail page reads\n // /api/app/orders/{id} or /api/app/orders?id=… for a single record.\n const sqlUrlKey_cmp_msyfmegt_jkkn3 = readUrlKey(\"query\", sqlKeyParam_cmp_msyfmegt_jkkn3)\n const sqlSelectUrl_cmp_msyfmegt_jkkn3 = React.useMemo(() => {\n if (!sqlselect_cmp_msyfmegt_jkkn3) return \"\"\n const base = sqlUrlKey_cmp_msyfmegt_jkkn3\n ? (() => {\n const bound = bindSqlUrl(sqlselect_cmp_msyfmegt_jkkn3.url, { [sqlKeyParam_cmp_msyfmegt_jkkn3]: sqlUrlKey_cmp_msyfmegt_jkkn3 }, sqlKeyParam_cmp_msyfmegt_jkkn3)\n return bound.keyBound ? bound.url : appendQueryParam(bound.url, sqlKeyParam_cmp_msyfmegt_jkkn3, sqlUrlKey_cmp_msyfmegt_jkkn3)\n })()\n : sqlselect_cmp_msyfmegt_jkkn3.url\n return base\n }, [sqlUrlKey_cmp_msyfmegt_jkkn3])\n // A key in the page URL always narrows the result: a list endpoint ignores the\n // parameter server side, so the requested record is picked out here. Without a\n // key every row is kept and the navigation below takes over.\n const sqlRows_cmp_msyfmegt_jkkn3 = React.useMemo(\n () => filterSqlRowsByKey(toSqlRows(data_source_5w_5ibpv, \"\"), sqlKey_cmp_msyfmegt_jkkn3, sqlUrlKey_cmp_msyfmegt_jkkn3),\n [data_source_5w_5ibpv, sqlUrlKey_cmp_msyfmegt_jkkn3],\n )\n // A fresh result set resets the position; New mode is left untouched so an\n // unsaved draft is not overwritten by a re-render.\n React.useEffect(() => {\n setSqlIndex_cmp_msyfmegt_jkkn3((current) => (current < sqlRows_cmp_msyfmegt_jkkn3.length ? current : 0))\n }, [sqlRows_cmp_msyfmegt_jkkn3])\n React.useEffect(() => {\n const row = sqlRows_cmp_msyfmegt_jkkn3[sqlIndex_cmp_msyfmegt_jkkn3]\n if (!row) return\n setData_cmp_msyfmegt_jkkn3(row)\n setSqlOriginal_cmp_msyfmegt_jkkn3(row)\n setSqlMode_cmp_msyfmegt_jkkn3(\"edit\")\n }, [sqlRows_cmp_msyfmegt_jkkn3, sqlIndex_cmp_msyfmegt_jkkn3])\n const sqlHasKey_cmp_msyfmegt_jkkn3 = (() => {\n const value = readSqlField(data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3)\n return value !== undefined && value !== null && value !== \"\"\n })()\n const sqlSetField_cmp_msyfmegt_jkkn3 = React.useCallback((path, value) => setData_cmp_msyfmegt_jkkn3((current) => setSqlField(current, path, value)), [])\n const sqlReload_cmp_msyfmegt_jkkn3 = React.useCallback(async () => {\n // Without the Read permission the container is not rendered at all; the\n // request is dropped here so it is not fired for a form nobody can see.\n if (!sqlCanSelect_cmp_msyfmegt_jkkn3) return\n if (!sqlSelectUrl_cmp_msyfmegt_jkkn3) return\n // Calling a URL that still holds a placeholder is a guaranteed 400, so the\n // missing key is reported in the component instead.\n if (hasSqlUrlParams(sqlSelectUrl_cmp_msyfmegt_jkkn3)) {\n throw new Error(translate(\"::App.Form.MissingKeyParam\", { 0: sqlKeyParam_cmp_msyfmegt_jkkn3 }))\n }\n const response = await apiService.fetchData({ url: sqlSelectUrl_cmp_msyfmegt_jkkn3, method: \"GET\" })\n setData_source_5w_5ibpv(getByPath(response.data, sqlselect_cmp_msyfmegt_jkkn3.responsePath))\n }, [sqlSelectUrl_cmp_msyfmegt_jkkn3])\n // Reload wrapped with the busy/error handling the toolbar and mount effect need,\n // so a failing Select is reported in the component instead of the console.\n const sqlRefresh_cmp_msyfmegt_jkkn3 = React.useCallback(async () => {\n setSqlBusy_cmp_msyfmegt_jkkn3(true)\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n try {\n await sqlReload_cmp_msyfmegt_jkkn3()\n } catch (error) {\n setSqlError_cmp_msyfmegt_jkkn3(toSqlErrorMessage(error))\n } finally {\n setSqlBusy_cmp_msyfmegt_jkkn3(false)\n }\n }, [sqlReload_cmp_msyfmegt_jkkn3])\n const sqlNew_cmp_msyfmegt_jkkn3 = React.useCallback(() => {\n if (!sqlCanInsert_cmp_msyfmegt_jkkn3) return\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n // Defaults are resolved on every New, so an @today column carries the day the\n // record was opened rather than the day the component was generated.\n const sqlDraft_cmp_msyfmegt_jkkn3 = buildSqlDefaults([{\"path\":\"IsDefault\",\"value\":true},{\"path\":\"IsStatic\",\"value\":true},{\"path\":\"IsPublic\",\"value\":false},{\"path\":\"EntityVersion\",\"value\":3},{\"path\":\"ExtraProperties\",\"value\":\"{}\"}])\n setData_cmp_msyfmegt_jkkn3(sqlDraft_cmp_msyfmegt_jkkn3)\n setSqlOriginal_cmp_msyfmegt_jkkn3({})\n setSqlMode_cmp_msyfmegt_jkkn3(\"new\")\n // After the re-render that paints the empty form, not before it.\n focusFirstSqlField(sqlHost_cmp_msyfmegt_jkkn3)\n }, [])\n const sqlPrev_cmp_msyfmegt_jkkn3 = React.useCallback(() => setSqlIndex_cmp_msyfmegt_jkkn3((current) => Math.max(0, current - 1)), [])\n const sqlNext_cmp_msyfmegt_jkkn3 = React.useCallback(() => setSqlIndex_cmp_msyfmegt_jkkn3((current) => Math.min(sqlRows_cmp_msyfmegt_jkkn3.length - 1, current + 1)), [sqlRows_cmp_msyfmegt_jkkn3])\n const sqlSave_cmp_msyfmegt_jkkn3 = async () => {\n const isNew = sqlMode_cmp_msyfmegt_jkkn3 === \"new\"\n const target = isNew ? sqlinsert_cmp_msyfmegt_jkkn3 : sqlupdate_cmp_msyfmegt_jkkn3\n // The button is already hidden without the permission; a script calling save\n // through a ref must not be able to walk around it either.\n if (!(isNew ? sqlCanInsert_cmp_msyfmegt_jkkn3 : sqlCanUpdate_cmp_msyfmegt_jkkn3)) return\n if (!target) {\n setSqlError_cmp_msyfmegt_jkkn3(isNew ? translate(\"::App.Form.InsertEndpointMissing\") : translate(\"::App.Form.UpdateEndpointMissing\"))\n return\n }\n // Update carries only the edited columns plus the key; Insert sends the record.\n const payload = isNew ? data_cmp_msyfmegt_jkkn3 : toSqlChanges(sqlOriginal_cmp_msyfmegt_jkkn3, data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3)\n if (!isNew && Object.keys(payload).filter((column) => column.toLowerCase() !== String(sqlKey_cmp_msyfmegt_jkkn3).toLowerCase()).length === 0) {\n setSqlError_cmp_msyfmegt_jkkn3(translate(\"::App.Form.NoChanges\"))\n return\n }\n setSqlBusy_cmp_msyfmegt_jkkn3(true)\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n try {\n const response = await callSqlEndpoint(target, data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3, payload)\n // With a Select endpoint the reload is the source of truth and its effect\n // repopulates the form; without one, the response row is all there is.\n if (sqlselect_cmp_msyfmegt_jkkn3) {\n await sqlReload_cmp_msyfmegt_jkkn3()\n } else {\n const saved = toSqlRows(response?.data, \"\")[0]\n if (saved) {\n setData_cmp_msyfmegt_jkkn3(saved)\n setSqlOriginal_cmp_msyfmegt_jkkn3(saved)\n }\n }\n setSqlMode_cmp_msyfmegt_jkkn3(\"edit\")\n } catch (error) {\n setSqlError_cmp_msyfmegt_jkkn3(toSqlErrorMessage(error))\n } finally {\n setSqlBusy_cmp_msyfmegt_jkkn3(false)\n }\n }\n const sqlDelete_cmp_msyfmegt_jkkn3 = async () => {\n if (!sqlCanDelete_cmp_msyfmegt_jkkn3 || !sqldelete_cmp_msyfmegt_jkkn3 || !sqlHasKey_cmp_msyfmegt_jkkn3 || sqlMode_cmp_msyfmegt_jkkn3 === \"new\") return\n // Deleting cannot be undone from the form, so it always asks first.\n const confirmMessage = translate(\"::App.Form.DeleteConfirm\", { 0: sqlKey_cmp_msyfmegt_jkkn3 + \" = \" + readSqlField(data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3) })\n if (typeof window !== \"undefined\" && !window.confirm(confirmMessage)) return\n setSqlBusy_cmp_msyfmegt_jkkn3(true)\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n try {\n await callSqlEndpoint(sqldelete_cmp_msyfmegt_jkkn3, data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3)\n setData_cmp_msyfmegt_jkkn3({})\n setSqlOriginal_cmp_msyfmegt_jkkn3({})\n setSqlMode_cmp_msyfmegt_jkkn3(\"new\")\n await sqlReload_cmp_msyfmegt_jkkn3()\n } catch (error) {\n setSqlError_cmp_msyfmegt_jkkn3(toSqlErrorMessage(error))\n } finally {\n setSqlBusy_cmp_msyfmegt_jkkn3(false)\n }\n }\n React.useEffect(() => { void sqlRefresh_cmp_msyfmegt_jkkn3() }, [sqlRefresh_cmp_msyfmegt_jkkn3])\n\n const filters_source_tq_veb4m = [{ param: \"RoleId\", value: readSqlField(data_cmp_msyfmegt_jkkn3, \"id\"), required: true }]\n const filterKey_source_tq_veb4m = JSON.stringify(filters_source_tq_veb4m)\n React.useEffect(() => {\n const url = buildFilteredUrl(\"/api/app/crudendpoint/AbpUserRoles\", filters_source_tq_veb4m)\n // An unmet required filter leaves the component empty on purpose.\n if (!url) {\n setData_source_tq_veb4m(null)\n return\n }\n let active = true\n apiService.fetchData({ url, method: 'GET' }).then((response) => {\n if (active) setData_source_tq_veb4m(getByPath(response.data, \"\"))\n }).catch((error) => console.error(\"AbpUserRoles · GetList endpoint error:\", error))\n return () => { active = false }\n }, [filterKey_source_tq_veb4m])\n\n React.useEffect(() => {\n let active = true\n apiService.fetchData({ url: \"/api/app/crudendpoint/AbpUsers\", method: 'GET' }).then((response) => {\n if (active) setData_source_gq_0q1dz(getByPath(response.data, \"\"))\n }).catch((error) => console.error(\"AbpUsers · GetList endpoint error:\", error))\n return () => { active = false }\n }, [])\n\n const platformFilters_cmp_mt0288i3_ebpj5 = [{ field: \"Id\", operator: \"=\", kind: \"eq\", value: readSqlField(data_cmp_msyfmegt_jkkn3, \"Id\"), required: true }]\n const platformKey_cmp_mt0288i3_ebpj5 = JSON.stringify(platformFilters_cmp_mt0288i3_ebpj5)\n // The views keep the params object in their own state and write back to it, so\n // it must stay the same object until a filter value actually changes.\n const platformParams_cmp_mt0288i3_ebpj5 = React.useMemo(() => buildPlatformFilterParams(platformFilters_cmp_mt0288i3_ebpj5), [platformKey_cmp_mt0288i3_ebpj5])\n\n const handle_cmp_msyfpfia_ifhdq_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"Name\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyjl707_crcn7_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"NormalizedName\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyjdodn_u3o74_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"ConcurrencyStamp\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyje9ys_2c08a_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"IsDefault\", typeof originalEvent === \"object\" && originalEvent?.target ? Boolean(originalEvent.target.checked) : typeof valueOrEvent === \"object\" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))\n const event = { checked: Boolean(valueOrEvent), originalEvent, target: originalEvent?.target }\n }\n\n const handle_cmp_msyjebu2_27tqi_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"IsStatic\", typeof originalEvent === \"object\" && originalEvent?.target ? Boolean(originalEvent.target.checked) : typeof valueOrEvent === \"object\" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))\n const event = { checked: Boolean(valueOrEvent), originalEvent, target: originalEvent?.target }\n }\n\n const handle_cmp_msyjmetn_gojol_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"IsPublic\", typeof originalEvent === \"object\" && originalEvent?.target ? Boolean(originalEvent.target.checked) : typeof valueOrEvent === \"object\" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))\n const event = { checked: Boolean(valueOrEvent), originalEvent, target: originalEvent?.target }\n }\n\n const handle_cmp_msyjo0g8_oathk_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"EntityVersion\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyjplfq_py8lz_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"ExtraProperties\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n return (\n <>\n {sqlCanSelect_cmp_msyfmegt_jkkn3 ? (\n {sqlHasKey_cmp_msyfmegt_jkkn3 ? sqlKey_cmp_msyfmegt_jkkn3 + \": \" + readSqlField(data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3) : sqlMode_cmp_msyfmegt_jkkn3 === \"new\" ? translate(\"::App.Platform.NewRecord\") : \"\"}}>\n
\n
\n \n \n \n \n {\"Is Default\"}\n \n \n {\"Is Static\"}\n \n \n {\"Is Public\"}\n \n \n \n
\n
\n {sqlRows_cmp_msyfmegt_jkkn3.length > 1 ? (\n <>\n \n {`${sqlIndex_cmp_msyfmegt_jkkn3 + 1} / ${sqlRows_cmp_msyfmegt_jkkn3.length}`}\n \n \n \n ) : null}\n {sqlinsert_cmp_msyfmegt_jkkn3 && sqlCanInsert_cmp_msyfmegt_jkkn3 ? : null}\n {(sqlMode_cmp_msyfmegt_jkkn3 === \"new\" ? sqlinsert_cmp_msyfmegt_jkkn3 && sqlCanInsert_cmp_msyfmegt_jkkn3 : sqlupdate_cmp_msyfmegt_jkkn3 && sqlCanUpdate_cmp_msyfmegt_jkkn3) ? : null}\n {sqldelete_cmp_msyfmegt_jkkn3 && sqlCanDelete_cmp_msyfmegt_jkkn3 ? : null}\n {sqlselect_cmp_msyfmegt_jkkn3 && sqlCanSelect_cmp_msyfmegt_jkkn3 ? : null}\n {sqlMode_cmp_msyfmegt_jkkn3 === \"new\" ? translate(\"::App.Platform.NewRecord\") : translate(\"::App.Platform.Editing\")}\n
\n {sqlError_cmp_msyfmegt_jkkn3 ?
{sqlError_cmp_msyfmegt_jkkn3}
: null}\n
\n
\n ) : null}\n
\n {(() => {\n const gridItems_cmp_mt023r5u_rb2jv = (Array.isArray(getByPath(data_source_tq_veb4m, \"\")) ? getByPath(data_source_tq_veb4m, \"\") : Array.isArray(data_source_tq_veb4m) ? data_source_tq_veb4m : null)\n // Nothing loaded yet — an endpoint still in flight, or a required filter\n // holding the request back — is an empty grid, not a binding error.\n if (gridItems_cmp_mt023r5u_rb2jv === null || gridItems_cmp_mt023r5u_rb2jv === undefined) return null\n if (!Array.isArray(gridItems_cmp_mt023r5u_rb2jv)) return
Grid items bağlantısı bir koleksiyon döndürmelidir.
\n const gridColumns_cmp_mt023r5u_rb2jv = [\"UserId\"]\n const gridLookups_cmp_mt023r5u_rb2jv = { \"UserId\": { rows: data_source_gq_0q1dz, path: \"\", value: \"Id\", text: \"UserName\" } }\n if (!gridColumns_cmp_mt023r5u_rb2jv.length) return
Preview için en az bir sütun seçin.
\n return (\n
\n \n \n {gridColumns_cmp_mt023r5u_rb2jv.map((column) => )}\n \n \n {gridItems_cmp_mt023r5u_rb2jv.map((gridItem_cmp_mt023r5u_rb2jv, rowIndex) => (\n \n {gridColumns_cmp_mt023r5u_rb2jv.map((column) => {\n const cellValue_cmp_mt023r5u_rb2jv = column === \"value\" ? gridItem_cmp_mt023r5u_rb2jv : getByPath(gridItem_cmp_mt023r5u_rb2jv, column)\n const lookedUp = resolveLookupText(gridLookups_cmp_mt023r5u_rb2jv[column], cellValue_cmp_mt023r5u_rb2jv)\n const cellText = lookedUp === undefined ? cellValue_cmp_mt023r5u_rb2jv : lookedUp\n const text = typeof cellText === \"object\" && cellText !== null && !(cellText instanceof Date) ? JSON.stringify(cellText) : formatLocaleValue(cellText) || \"—\"\n return \n })}\n \n ))}\n \n
{(column === \"value\" ? \"Value\" : column)}
{text}
\n
\n )\n })()}\n
\n {platformParams_cmp_mt0288i3_ebpj5 === null ? null : (\n \n )}\n \n )\n}\n\nexport default RoleComponent", - "Props": "{\"visualDesigner\":{\"version\":1,\"sourceMode\":\"visual\",\"nodes\":[{\"id\":\"cmp_msyfmegt_jkkn3\",\"type\":\"Form\",\"kind\":\"layout\",\"props\":{\"selectEndpoint\":\"source_5w_5ibpv\",\"insertEndpoint\":\"source_h3_484qa\",\"updateEndpoint\":\"source_gb_d9cvs\",\"deleteEndpoint\":\"source_0t_avok9\",\"keyFieldName\":\"id\",\"collectionPath\":\"\",\"keySource\":\"query\",\"keyParamName\":\"id\",\"previewKeyValue\":\"\",\"autoLoad\":true,\"showToolbar\":true,\"gap\":16,\"className\":\"\",\"title\":\"::AbpIdentity.Roles\",\"columnCount\":2,\"selectPermission\":\"\",\"insertPermission\":\"\"},\"events\":{\"onLoad\":\"\",\"onRecordChange\":\"\",\"onFieldChange\":\"\",\"onNewRecord\":\"\",\"onModeChange\":\"\",\"onBeforeSave\":\"\",\"onAfterSave\":\"\",\"onBeforeDelete\":\"\",\"onAfterDelete\":\"\",\"onError\":\"\"},\"bindings\":{},\"children\":[{\"id\":\"cmp_msyfpfia_ifhdq\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"::App.Platform.Name\",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\",\"sqlDefaultValue\":\"\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"Name\"}},\"children\":[],\"ref\":\"input1\"},{\"id\":\"cmp_msyjl707_crcn7\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Normalized Name :\",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"NormalizedName\"}},\"children\":[],\"ref\":\"input2\"},{\"id\":\"cmp_msyjdodn_u3o74\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Concurrent Stamp : \",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"ConcurrencyStamp\"}},\"children\":[],\"ref\":\"input3\"},{\"id\":\"cmp_msyje9ys_2c08a\",\"type\":\"Checkbox\",\"kind\":\"ui\",\"props\":{\"checked\":false,\"color\":\"blue-500\",\"disabled\":false,\"labelRef\":\"\",\"name\":\"\",\"readOnly\":false,\"value\":\"\",\"field\":null,\"className\":\"\",\"children\":\"Is Default\",\"style\":{},\"sqlDefaultValue\":\"true\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"checked\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"IsDefault\"}},\"children\":[],\"ref\":\"checkbox1\"},{\"id\":\"cmp_msyjebu2_27tqi\",\"type\":\"Checkbox\",\"kind\":\"ui\",\"props\":{\"checked\":false,\"color\":\"blue-500\",\"disabled\":false,\"labelRef\":\"\",\"name\":\"\",\"readOnly\":false,\"value\":\"\",\"field\":null,\"className\":\"\",\"children\":\"Is Static\",\"style\":{},\"sqlDefaultValue\":\"true\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"checked\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"IsStatic\"}},\"children\":[],\"ref\":\"checkbox2\"},{\"id\":\"cmp_msyjmetn_gojol\",\"type\":\"Checkbox\",\"kind\":\"ui\",\"props\":{\"checked\":false,\"color\":\"blue-500\",\"disabled\":false,\"labelRef\":\"\",\"name\":\"\",\"readOnly\":false,\"value\":\"\",\"field\":null,\"className\":\"\",\"children\":\"Is Public\",\"style\":{},\"sqlDefaultValue\":\"false\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"checked\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"IsPublic\"}},\"children\":[],\"ref\":\"checkbox3\"},{\"id\":\"cmp_msyjo0g8_oathk\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Entity Version : \",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"number\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\",\"sqlDefaultValue\":\"3\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"EntityVersion\"}},\"children\":[],\"ref\":\"input4\"},{\"id\":\"cmp_msyjplfq_py8lz\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Extra Properties :\",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\",\"sqlDefaultValue\":\"{}\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"ExtraProperties\"}},\"children\":[],\"ref\":\"input5\"}],\"ref\":\"form1\"},{\"id\":\"cmp_mt023r5u_rb2jv\",\"type\":\"Grid\",\"kind\":\"ui\",\"props\":{\"hoverable\":true,\"overflow\":true,\"borderlessRow\":false,\"compact\":false,\"cols\":\"3\",\"rows\":\"2\",\"gap\":\"4\",\"colGap\":\"4\",\"rowGap\":\"4\",\"responsive\":true,\"smCols\":\"1\",\"mdCols\":\"2\",\"lgCols\":\"3\",\"xlCols\":\"4\",\"autoFit\":false,\"minColWidth\":\"200px\",\"height\":\"auto\",\"className\":\"mt-4\",\"items\":[{\"id\":\"grid-item-1\",\"content\":\"Grid Item 1\",\"colSpan\":1,\"rowSpan\":1,\"colStart\":1,\"rowStart\":1,\"className\":\"bg-blue-100 p-4 border rounded\"},{\"id\":\"grid-item-2\",\"content\":\"Grid Item 2\",\"colSpan\":1,\"rowSpan\":1,\"colStart\":2,\"rowStart\":1,\"className\":\"bg-green-100 p-4 border rounded\"},{\"id\":\"grid-item-3\",\"content\":\"Grid Item 3\",\"colSpan\":1,\"rowSpan\":1,\"colStart\":3,\"rowStart\":1,\"className\":\"bg-yellow-100 p-4 border rounded\"},{\"id\":\"grid-item-4\",\"content\":\"Grid Item 4\",\"colSpan\":2,\"rowSpan\":1,\"colStart\":1,\"rowStart\":2,\"className\":\"bg-purple-100 p-4 border rounded\"},{\"id\":\"grid-item-5\",\"content\":\"Grid Item 5\",\"colSpan\":1,\"rowSpan\":1,\"colStart\":3,\"rowStart\":2,\"className\":\"bg-red-100 p-4 border rounded\"}],\"dataColumns\":[\"UserId\"],\"columnLookups\":{\"UserId\":{\"sourceId\":\"source_gq_0q1dz\",\"path\":\"\",\"valueField\":\"Id\",\"textField\":\"UserName\"}},\"columnCaptions\":{}},\"events\":{},\"bindings\":{\"items\":{\"sourceId\":\"source_tq_veb4m\",\"path\":\"\"}},\"children\":[],\"ref\":\"grid1\"},{\"id\":\"cmp_mt0288i3_ebpj5\",\"type\":\"ListView\",\"kind\":\"platform\",\"props\":{\"listFormCode\":\"AbpIdentity.Roles\",\"height\":\"520px\",\"filters\":[{\"id\":\"filter_y9_tr73w\",\"field\":\"Id\",\"operator\":\"eq\",\"source\":\"record\",\"value\":\"form1.Id\",\"previewValue\":\"\",\"required\":true}],\"className\":\"mt-4\"},\"events\":{},\"bindings\":{},\"children\":[],\"ref\":\"listView1\"}],\"canvas\":{\"width\":\"responsive\"},\"lifecycle\":{\"onMount\":\"\"},\"dataSources\":[{\"id\":\"source_5w_5ibpv\",\"name\":\"AbpRoles · GetList\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpRoles\",\"responsePath\":\"\"},{\"id\":\"source_i7_msox4\",\"name\":\"AbpRoles · GetById\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpRoles/{id}\",\"responsePath\":\"\"},{\"id\":\"source_0t_avok9\",\"name\":\"AbpRoles · Delete\",\"method\":\"DELETE\",\"url\":\"/api/app/crudendpoint/AbpRoles/{id}\",\"responsePath\":\"\"},{\"id\":\"source_h3_484qa\",\"name\":\"AbpRoles · Create\",\"method\":\"POST\",\"url\":\"/api/app/crudendpoint/AbpRoles\",\"responsePath\":\"\"},{\"id\":\"source_gb_d9cvs\",\"name\":\"AbpRoles · Update\",\"method\":\"PUT\",\"url\":\"/api/app/crudendpoint/AbpRoles/{id}\",\"responsePath\":\"\"},{\"id\":\"source_tq_veb4m\",\"name\":\"AbpUserRoles · GetList\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpUserRoles\",\"responsePath\":\"\",\"filters\":[{\"id\":\"filter_sw_86e0q\",\"field\":\"RoleId\",\"operator\":\"eq\",\"source\":\"record\",\"value\":\"form1.id\",\"previewValue\":\"\",\"required\":true}]},{\"id\":\"source_d2_bri5s\",\"name\":\"AbpUserRoles · GetById\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpUserRoles/{id}\",\"responsePath\":\"\"},{\"id\":\"source_gq_0q1dz\",\"name\":\"AbpUsers · GetList\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpUsers\",\"responsePath\":\"\"},{\"id\":\"source_u8_nu1lu\",\"name\":\"AbpUsers · GetById\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpUsers/{id}\",\"responsePath\":\"\"}],\"permissionCode\":\"\"}}", - "DataSources": [ - { - "Name": "AbpRoles · GetList", - "Method": "GET", - "Url": "/api/app/crudendpoint/AbpRoles", - "ResponsePath": null, - "EntityName": "AbpRoles", - "OperationType": "GetList", - "SeedFile": "crud/AbpRoles.json" - }, - { - "Name": "AbpRoles · GetById", - "Method": "GET", - "Url": "/api/app/crudendpoint/AbpRoles/{id}", - "ResponsePath": null, - "EntityName": "AbpRoles", - "OperationType": "GetById", - "SeedFile": "crud/AbpRoles.json" - }, - { - "Name": "AbpRoles · Delete", - "Method": "DELETE", - "Url": "/api/app/crudendpoint/AbpRoles/{id}", - "ResponsePath": null, - "EntityName": "AbpRoles", - "OperationType": "Delete", - "SeedFile": "crud/AbpRoles.json" - }, - { - "Name": "AbpRoles · Create", - "Method": "POST", - "Url": "/api/app/crudendpoint/AbpRoles", - "ResponsePath": null, - "EntityName": "AbpRoles", - "OperationType": "Create", - "SeedFile": "crud/AbpRoles.json" - }, - { - "Name": "AbpRoles · Update", - "Method": "PUT", - "Url": "/api/app/crudendpoint/AbpRoles/{id}", - "ResponsePath": null, - "EntityName": "AbpRoles", - "OperationType": "Update", - "SeedFile": "crud/AbpRoles.json" - }, - { - "Name": "AbpUserRoles · GetList", - "Method": "GET", - "Url": "/api/app/crudendpoint/AbpUserRoles", - "ResponsePath": null, - "EntityName": "AbpUserRoles", - "OperationType": "GetList", - "SeedFile": "crud/AbpUserRoles.json" - }, - { - "Name": "AbpUserRoles · GetById", - "Method": "GET", - "Url": "/api/app/crudendpoint/AbpUserRoles/{id}", - "ResponsePath": null, - "EntityName": "AbpUserRoles", - "OperationType": "GetById", - "SeedFile": "crud/AbpUserRoles.json" - }, - { - "Name": "AbpUsers · GetList", - "Method": "GET", - "Url": "/api/app/crudendpoint/AbpUsers", - "ResponsePath": null, - "EntityName": "AbpUsers", - "OperationType": "GetList", - "SeedFile": "crud/AbpUsers.json" - }, - { - "Name": "AbpUsers · GetById", - "Method": "GET", - "Url": "/api/app/crudendpoint/AbpUsers/{id}", - "ResponsePath": null, - "EntityName": "AbpUsers", - "OperationType": "GetById", - "SeedFile": "crud/AbpUsers.json" - } - ] + "Dependencies": "[]", + "Code": "/*__SOZSOFT_VISUAL_DESIGNER__%7B%22version%22%3A1%2C%22sourceMode%22%3A%22visual%22%2C%22nodes%22%3A%5B%7B%22id%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22type%22%3A%22Form%22%2C%22kind%22%3A%22layout%22%2C%22props%22%3A%7B%22selectEndpoint%22%3A%22source_5w_5ibpv%22%2C%22insertEndpoint%22%3A%22source_h3_484qa%22%2C%22updateEndpoint%22%3A%22source_gb_d9cvs%22%2C%22deleteEndpoint%22%3A%22source_0t_avok9%22%2C%22keyFieldName%22%3A%22id%22%2C%22collectionPath%22%3A%22%22%2C%22keySource%22%3A%22query%22%2C%22keyParamName%22%3A%22id%22%2C%22previewKeyValue%22%3A%22%22%2C%22autoLoad%22%3Atrue%2C%22showToolbar%22%3Atrue%2C%22gap%22%3A16%2C%22className%22%3A%22%22%2C%22title%22%3A%22%3A%3AAbpIdentity.Roles%22%2C%22columnCount%22%3A2%2C%22selectPermission%22%3A%22%22%2C%22insertPermission%22%3A%22%22%7D%2C%22events%22%3A%7B%22onLoad%22%3A%22%22%2C%22onRecordChange%22%3A%22%22%2C%22onFieldChange%22%3A%22%22%2C%22onNewRecord%22%3A%22%22%2C%22onModeChange%22%3A%22%22%2C%22onBeforeSave%22%3A%22%22%2C%22onAfterSave%22%3A%22%22%2C%22onBeforeDelete%22%3A%22%22%2C%22onAfterDelete%22%3A%22%22%2C%22onError%22%3A%22%22%7D%2C%22bindings%22%3A%7B%7D%2C%22children%22%3A%5B%7B%22id%22%3A%22cmp_msyfpfia_ifhdq%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22%3A%3AApp.Platform.Name%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%2C%22sqlDefaultValue%22%3A%22%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22Name%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input1%22%7D%2C%7B%22id%22%3A%22cmp_msyjl707_crcn7%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Normalized%20Name%20%3A%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22NormalizedName%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input2%22%7D%2C%7B%22id%22%3A%22cmp_msyjdodn_u3o74%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Concurrent%20Stamp%20%3A%20%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22ConcurrencyStamp%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input3%22%7D%2C%7B%22id%22%3A%22cmp_msyje9ys_2c08a%22%2C%22type%22%3A%22Checkbox%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22checked%22%3Afalse%2C%22color%22%3A%22blue-500%22%2C%22disabled%22%3Afalse%2C%22labelRef%22%3A%22%22%2C%22name%22%3A%22%22%2C%22readOnly%22%3Afalse%2C%22value%22%3A%22%22%2C%22field%22%3Anull%2C%22className%22%3A%22%22%2C%22children%22%3A%22Is%20Default%22%2C%22style%22%3A%7B%7D%2C%22sqlDefaultValue%22%3A%22true%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22checked%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22IsDefault%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22checkbox1%22%7D%2C%7B%22id%22%3A%22cmp_msyjebu2_27tqi%22%2C%22type%22%3A%22Checkbox%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22checked%22%3Afalse%2C%22color%22%3A%22blue-500%22%2C%22disabled%22%3Afalse%2C%22labelRef%22%3A%22%22%2C%22name%22%3A%22%22%2C%22readOnly%22%3Afalse%2C%22value%22%3A%22%22%2C%22field%22%3Anull%2C%22className%22%3A%22%22%2C%22children%22%3A%22Is%20Static%22%2C%22style%22%3A%7B%7D%2C%22sqlDefaultValue%22%3A%22true%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22checked%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22IsStatic%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22checkbox2%22%7D%2C%7B%22id%22%3A%22cmp_msyjmetn_gojol%22%2C%22type%22%3A%22Checkbox%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22checked%22%3Afalse%2C%22color%22%3A%22blue-500%22%2C%22disabled%22%3Afalse%2C%22labelRef%22%3A%22%22%2C%22name%22%3A%22%22%2C%22readOnly%22%3Afalse%2C%22value%22%3A%22%22%2C%22field%22%3Anull%2C%22className%22%3A%22%22%2C%22children%22%3A%22Is%20Public%22%2C%22style%22%3A%7B%7D%2C%22sqlDefaultValue%22%3A%22false%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22checked%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22IsPublic%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22checkbox3%22%7D%2C%7B%22id%22%3A%22cmp_msyjo0g8_oathk%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Entity%20Version%20%3A%20%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22number%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%2C%22sqlDefaultValue%22%3A%223%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22EntityVersion%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input4%22%7D%2C%7B%22id%22%3A%22cmp_msyjplfq_py8lz%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Extra%20Properties%20%3A%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%2C%22sqlDefaultValue%22%3A%22%7B%7D%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22ExtraProperties%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input5%22%7D%5D%2C%22ref%22%3A%22form1%22%7D%2C%7B%22id%22%3A%22cmp_mubctlbp_p6ajt%22%2C%22type%22%3A%22BarcodeScanField%22%2C%22kind%22%3A%22custom%22%2C%22props%22%3A%7B%22label%22%3A%22%22%2C%22name%22%3A%22barcode%22%2C%22placeholder%22%3A%22%22%2C%22buttonText%22%3A%22%22%2C%22continuous%22%3Atrue%2C%22readOnly%22%3Atrue%2C%22className%22%3A%22mt-4%22%7D%2C%22events%22%3A%7B%7D%2C%22bindings%22%3A%7B%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22barcodeScanField1%22%7D%2C%7B%22id%22%3A%22cmp_mt023r5u_rb2jv%22%2C%22type%22%3A%22Grid%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22hoverable%22%3Atrue%2C%22overflow%22%3Atrue%2C%22borderlessRow%22%3Afalse%2C%22compact%22%3Afalse%2C%22cols%22%3A%223%22%2C%22rows%22%3A%222%22%2C%22gap%22%3A%224%22%2C%22colGap%22%3A%224%22%2C%22rowGap%22%3A%224%22%2C%22responsive%22%3Atrue%2C%22smCols%22%3A%221%22%2C%22mdCols%22%3A%222%22%2C%22lgCols%22%3A%223%22%2C%22xlCols%22%3A%224%22%2C%22autoFit%22%3Afalse%2C%22minColWidth%22%3A%22200px%22%2C%22height%22%3A%22auto%22%2C%22className%22%3A%22mt-4%22%2C%22items%22%3A%5B%7B%22id%22%3A%22grid-item-1%22%2C%22content%22%3A%22Grid%20Item%201%22%2C%22colSpan%22%3A1%2C%22rowSpan%22%3A1%2C%22colStart%22%3A1%2C%22rowStart%22%3A1%2C%22className%22%3A%22bg-blue-100%20p-4%20border%20rounded%22%7D%2C%7B%22id%22%3A%22grid-item-2%22%2C%22content%22%3A%22Grid%20Item%202%22%2C%22colSpan%22%3A1%2C%22rowSpan%22%3A1%2C%22colStart%22%3A2%2C%22rowStart%22%3A1%2C%22className%22%3A%22bg-green-100%20p-4%20border%20rounded%22%7D%2C%7B%22id%22%3A%22grid-item-3%22%2C%22content%22%3A%22Grid%20Item%203%22%2C%22colSpan%22%3A1%2C%22rowSpan%22%3A1%2C%22colStart%22%3A3%2C%22rowStart%22%3A1%2C%22className%22%3A%22bg-yellow-100%20p-4%20border%20rounded%22%7D%2C%7B%22id%22%3A%22grid-item-4%22%2C%22content%22%3A%22Grid%20Item%204%22%2C%22colSpan%22%3A2%2C%22rowSpan%22%3A1%2C%22colStart%22%3A1%2C%22rowStart%22%3A2%2C%22className%22%3A%22bg-purple-100%20p-4%20border%20rounded%22%7D%2C%7B%22id%22%3A%22grid-item-5%22%2C%22content%22%3A%22Grid%20Item%205%22%2C%22colSpan%22%3A1%2C%22rowSpan%22%3A1%2C%22colStart%22%3A3%2C%22rowStart%22%3A2%2C%22className%22%3A%22bg-red-100%20p-4%20border%20rounded%22%7D%5D%2C%22dataColumns%22%3A%5B%22UserId%22%5D%2C%22columnLookups%22%3A%7B%22UserId%22%3A%7B%22sourceId%22%3A%22source_gq_0q1dz%22%2C%22path%22%3A%22%22%2C%22valueField%22%3A%22Id%22%2C%22textField%22%3A%22UserName%22%7D%7D%2C%22columnCaptions%22%3A%7B%7D%7D%2C%22events%22%3A%7B%7D%2C%22bindings%22%3A%7B%22items%22%3A%7B%22sourceId%22%3A%22source_tq_veb4m%22%2C%22path%22%3A%22%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22grid1%22%7D%2C%7B%22id%22%3A%22cmp_mt0288i3_ebpj5%22%2C%22type%22%3A%22ListView%22%2C%22kind%22%3A%22platform%22%2C%22props%22%3A%7B%22listFormCode%22%3A%22AbpIdentity.Roles%22%2C%22height%22%3A%22520px%22%2C%22filters%22%3A%5B%7B%22id%22%3A%22filter_y9_tr73w%22%2C%22field%22%3A%22Id%22%2C%22operator%22%3A%22eq%22%2C%22source%22%3A%22record%22%2C%22value%22%3A%22form1.Id%22%2C%22previewValue%22%3A%22%22%2C%22required%22%3Atrue%7D%5D%2C%22className%22%3A%22mt-4%22%7D%2C%22events%22%3A%7B%7D%2C%22bindings%22%3A%7B%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22listView1%22%7D%5D%2C%22canvas%22%3A%7B%22width%22%3A%22responsive%22%7D%2C%22lifecycle%22%3A%7B%22onMount%22%3A%22%22%7D%2C%22dataSources%22%3A%5B%7B%22id%22%3A%22source_5w_5ibpv%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20GetList%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_i7_msox4%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20GetById%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_0t_avok9%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20Delete%22%2C%22method%22%3A%22DELETE%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_h3_484qa%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20Create%22%2C%22method%22%3A%22POST%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_gb_d9cvs%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20Update%22%2C%22method%22%3A%22PUT%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_tq_veb4m%22%2C%22name%22%3A%22AbpUserRoles%20%C2%B7%20GetList%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpUserRoles%22%2C%22responsePath%22%3A%22%22%2C%22filters%22%3A%5B%7B%22id%22%3A%22filter_sw_86e0q%22%2C%22field%22%3A%22RoleId%22%2C%22operator%22%3A%22eq%22%2C%22source%22%3A%22record%22%2C%22value%22%3A%22form1.id%22%2C%22previewValue%22%3A%22%22%2C%22required%22%3Atrue%7D%5D%7D%2C%7B%22id%22%3A%22source_d2_bri5s%22%2C%22name%22%3A%22AbpUserRoles%20%C2%B7%20GetById%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpUserRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_gq_0q1dz%22%2C%22name%22%3A%22AbpUsers%20%C2%B7%20GetList%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpUsers%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_u8_nu1lu%22%2C%22name%22%3A%22AbpUsers%20%C2%B7%20GetById%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpUsers%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%5D%2C%22permissionCode%22%3A%22%22%7D__*/\nconst RoleComponent = () => {\n const getByPath = (value, path) => {\n if (!path) return value\n const readPath = (target, targetPath) => targetPath.split('.').filter(Boolean).reduce((current, key) => current?.[key], target)\n const directValue = readPath(value, path)\n if (directValue !== undefined) return directValue\n if (path.startsWith('result.')) return readPath(value, path.slice('result.'.length))\n return value?.result === undefined ? undefined : readPath(value.result, path)\n }\n\n const resolveLookupText = (lookup, value) => {\n if (!lookup || value === null || value === undefined || value === \"\") return undefined\n const rows = getByPath(lookup.rows, lookup.path)\n if (!Array.isArray(rows)) return undefined\n const key = String(value)\n const row = rows.find((item) => item && typeof item === \"object\" && String(getByPath(item, lookup.value) ?? \"\") === key)\n if (!row) return undefined\n const text = getByPath(row, lookup.text)\n return text === null || text === undefined || text === \"\" ? undefined : text\n }\n\n const localeCulture = () => {\n const injected = typeof getCulture === \"function\" ? getCulture() : \"\"\n if (injected) return injected\n if (typeof document !== \"undefined\" && document.documentElement.lang) return document.documentElement.lang\n return undefined\n }\n // Patterns come from the culture itself; a fixed day/month/year order would\n // print the same layout in every language.\n const localeDateOptions = { dateStyle: \"short\" }\n const localeTimeOptions = { dateStyle: \"short\", timeStyle: \"short\" }\n const localeIsoPattern = /^(\\d{4})-(\\d{2})-(\\d{2})(?:[T ](\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d+)?)?(Z|[+-]\\d{2}:?\\d{2})?)?$/\n const formatLocaleValue = (value) => {\n if (value === null || value === undefined) return \"\"\n const culture = localeCulture()\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) return String(value)\n // Grouping and the decimal separator are the culture's decision; only the\n // precision guard stays, so a long decimal is not rounded away.\n return new Intl.NumberFormat(culture, { maximumFractionDigits: 20 }).format(value)\n }\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? \"\" : new Intl.DateTimeFormat(culture, localeTimeOptions).format(value)\n }\n if (typeof value !== \"string\") return String(value)\n const parts = localeIsoPattern.exec(value.trim())\n if (!parts) return value\n const parsed = new Date(value)\n if (Number.isNaN(parsed.getTime())) return value\n if (parts[4] === undefined) {\n const day = new Date(Number(parts[1]), Number(parts[2]) - 1, Number(parts[3]))\n return new Intl.DateTimeFormat(culture, localeDateOptions).format(day)\n }\n return new Intl.DateTimeFormat(culture, localeTimeOptions).format(parsed)\n }\n\n const readSqlField = (record, field) => {\n if (!record || typeof record !== \"object\" || Array.isArray(record)) return undefined\n if (field in record) return record[field]\n const matched = Object.keys(record).find((key) => key.toLowerCase() === String(field).toLowerCase())\n return matched === undefined ? undefined : record[matched]\n }\n const toSqlRows = (value, collectionPath) => {\n const source = collectionPath ? getByPath(value, collectionPath) : value\n const rows = Array.isArray(source)\n ? source\n : source && typeof source === \"object\"\n ? (Array.isArray(source.items) ? source.items : [source])\n : []\n return rows.filter((item) => item && typeof item === \"object\" && !Array.isArray(item))\n }\n const setSqlField = (record, path, value) => {\n const keys = String(path || \"\").split(\".\").filter(Boolean)\n if (!keys.length) return record\n const next = { ...(record || {}) }\n let target = next\n for (let index = 0; index < keys.length - 1; index += 1) {\n const child = target[keys[index]]\n target[keys[index]] = child && typeof child === \"object\" && !Array.isArray(child) ? { ...child } : {}\n target = target[keys[index]]\n }\n target[keys[keys.length - 1]] = value\n return next\n }\n const bindSqlUrl = (url, record, keyField) => {\n let keyBound = false\n const boundUrl = String(url).replace(/\\{([^}]+)\\}|(?<=\\/):([A-Za-z_][A-Za-z0-9_]*)/g, (match, braced, colon) => {\n const name = (braced || colon || \"\").trim()\n if (!name) return match\n const value = readSqlField(record, name) ?? readSqlField(record, keyField)\n if (value === undefined || value === null || value === \"\") return match\n if (name.toLowerCase() === String(keyField).toLowerCase()) keyBound = true\n return encodeURIComponent(String(value))\n })\n return { url: boundUrl, keyBound }\n }\n const appendQueryParam = (url, name, value) =>\n url + (url.includes(\"?\") ? \"&\" : \"?\") + encodeURIComponent(name) + \"=\" + encodeURIComponent(value)\n const hasSqlUrlParams = (url) => /\\{[^}]+\\}|(?<=\\/):[A-Za-z_][A-Za-z0-9_]*/.test(String(url))\n const readUrlKey = (source, name) => {\n if (typeof window === \"undefined\") return \"\"\n if (source === \"query\") return new URLSearchParams(window.location.search).get(name) ?? \"\"\n const segments = window.location.pathname.split(\"/\").filter(Boolean)\n return segments.length ? decodeURIComponent(segments[segments.length - 1]) : \"\"\n }\n const filterSqlRowsByKey = (rows, keyField, keyValue) => {\n if (!keyValue) return rows\n const matched = rows.filter((row) => String(readSqlField(row, keyField) ?? \"\") === String(keyValue))\n // No match means the endpoint already filtered server side, or the key column\n // is named differently — keeping the rows beats showing an empty form.\n return matched.length ? matched : rows\n }\n // Update payload: only the columns the user actually edited, plus the key.\n const toSqlChanges = (original, current, keyField) => {\n const changes = {}\n const isSame = (left, right) => JSON.stringify(left ?? null) === JSON.stringify(right ?? null)\n Object.keys(current || {}).forEach((column) => {\n if (!isSame(original ? original[column] : undefined, current[column])) changes[column] = current[column]\n })\n const keyColumn = Object.keys(current || {}).find((column) => column.toLowerCase() === String(keyField).toLowerCase())\n if (keyColumn !== undefined && changes[keyColumn] === undefined) changes[keyColumn] = current[keyColumn]\n return changes\n }\n const callSqlEndpoint = (target, record, keyField, payload) => {\n // The URL is always bound from the full record: a placeholder may reference a\n // column that the update payload does not carry.\n const bound = bindSqlUrl(target.url, record, keyField)\n const request = { url: bound.url, method: target.method }\n if (target.method === \"POST\" || target.method === \"PUT\") request.data = payload === undefined ? record : payload\n const keyValue = readSqlField(record, keyField)\n const hasKey = keyValue !== undefined && keyValue !== null && keyValue !== \"\"\n // The key still has to reach the endpoint when the URL carries no placeholder.\n if (!bound.keyBound && hasKey && target.method !== \"POST\") request.params = { [keyField]: keyValue }\n return apiService.fetchData(request)\n }\n // A new record starts at the top of the form: the first enabled editor takes the\n // cursor, so a Yeni click is immediately followed by typing.\n const focusFirstSqlField = (host) => {\n if (typeof window === \"undefined\") return\n window.requestAnimationFrame(() => {\n const field = host.current?.querySelector(\"input:not([type='hidden']):not([disabled]):not([readonly]), textarea:not([disabled]):not([readonly]), select:not([disabled])\")\n if (!field) return\n field.focus()\n if (typeof field.select === \"function\") field.select()\n })\n }\n // Defaults declared on the bound children; only a new record starts from them.\n const buildSqlDefaults = (fields) => fields.reduce((record, field) => setSqlField(record, field.path, resolveSqlDefault(field)), {})\n const resolveSqlDefault = (field) => {\n if (field.token !== \"today\" && field.token !== \"now\") return field.value\n const now = new Date()\n const pad = (value) => String(value).padStart(2, \"0\")\n const day = now.getFullYear() + \"-\" + pad(now.getMonth() + 1) + \"-\" + pad(now.getDate())\n if (field.token === \"today\" || field.dateOnly) return day\n return day + \"T\" + pad(now.getHours()) + \":\" + pad(now.getMinutes()) + \":\" + pad(now.getSeconds())\n }\n const toSqlErrorMessage = (error) => error?.response?.data?.error?.message || error?.response?.data?.message || error?.message || translate(\"::App.Platform.IslemBasarisiz\")\n\n // Value of a filter that reads the page URL: a query string parameter, or a\n // path segment — the one after a segment named like the parameter, otherwise\n // the last one, which is the /orders/{id} detail page shape.\n const readDesignerUrlValue = (kind, name) => {\n if (typeof window === \"undefined\") return \"\"\n if (kind === \"query\") return new URLSearchParams(window.location.search).get(name) ?? \"\"\n const segments = window.location.pathname.split(\"/\").filter(Boolean)\n if (!segments.length) return \"\"\n const index = name ? segments.findIndex((segment) => segment.toLowerCase() === String(name).toLowerCase()) : -1\n if (index >= 0 && index < segments.length - 1) return decodeURIComponent(segments[index + 1])\n return decodeURIComponent(segments[segments.length - 1])\n }\n // Filters are appended as query parameters: `Column` for an equality and\n // `Column.operator` for anything else, which is what the endpoint parses.\n // Returns \"\" when a required filter has no value yet — the caller must skip the\n // request instead of loading the unfiltered collection.\n const buildFilteredUrl = (url, filters) => {\n const parts = []\n for (const filter of filters || []) {\n const value = filter.value === undefined || filter.value === null ? \"\" : String(filter.value)\n if (!value) {\n if (filter.required) return \"\"\n continue\n }\n parts.push(encodeURIComponent(filter.param) + \"=\" + encodeURIComponent(value))\n }\n if (!parts.length) return url\n return url + (url.includes(\"?\") ? \"&\" : \"?\") + parts.join(\"&\")\n }\n\n const buildPlatformFilterParams = (filters) => {\n const expressions = []\n for (const filter of filters || []) {\n if (filter.kind === \"isnull\" || filter.kind === \"notnull\") {\n expressions.push([filter.field, filter.operator, null])\n continue\n }\n const value = filter.value === undefined || filter.value === null ? \"\" : String(filter.value)\n if (!value) {\n if (filter.required) return null\n continue\n }\n if (filter.kind === \"in\") {\n const entries = value.split(\",\").map((entry) => entry.trim()).filter(Boolean).map((entry) => [filter.field, \"=\", entry])\n if (!entries.length) continue\n expressions.push(entries.reduce((result, entry) => (result ? [result, \"or\", entry] : entry), null))\n continue\n }\n expressions.push([filter.field, filter.operator, value])\n }\n const params = new URLSearchParams()\n const merged = expressions.reduce((result, expression) => (result ? [result, \"and\", expression] : expression), null)\n if (merged) params.set(\"filter\", JSON.stringify(merged))\n return params\n }\n\n const [data_source_5w_5ibpv, setData_source_5w_5ibpv] = React.useState(null)\n const [data_source_i7_msox4, setData_source_i7_msox4] = React.useState(null)\n const [data_source_0t_avok9] = React.useState(null)\n const [data_source_h3_484qa] = React.useState(null)\n const [data_source_gb_d9cvs] = React.useState(null)\n const [data_source_tq_veb4m, setData_source_tq_veb4m] = React.useState(null)\n const [data_source_d2_bri5s, setData_source_d2_bri5s] = React.useState(null)\n const [data_source_gq_0q1dz, setData_source_gq_0q1dz] = React.useState(null)\n const [data_source_u8_nu1lu, setData_source_u8_nu1lu] = React.useState(null)\n\n const sqlselect_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles\", method: \"GET\", responsePath: \"\" }\n const sqlinsert_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles\", method: \"POST\", responsePath: \"\" }\n const sqlupdate_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles/{id}\", method: \"PUT\", responsePath: \"\" }\n const sqldelete_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles/{id}\", method: \"DELETE\", responsePath: \"\" }\n const sqlCanSelect_cmp_msyfmegt_jkkn3 = true\n const sqlCanInsert_cmp_msyfmegt_jkkn3 = true\n const sqlCanUpdate_cmp_msyfmegt_jkkn3 = true\n const sqlCanDelete_cmp_msyfmegt_jkkn3 = true\n const sqlKey_cmp_msyfmegt_jkkn3 = \"id\"\n const sqlKeyParam_cmp_msyfmegt_jkkn3 = \"id\"\n const [data_cmp_msyfmegt_jkkn3, setData_cmp_msyfmegt_jkkn3] = React.useState({})\n const [sqlOriginal_cmp_msyfmegt_jkkn3, setSqlOriginal_cmp_msyfmegt_jkkn3] = React.useState({})\n const [sqlMode_cmp_msyfmegt_jkkn3, setSqlMode_cmp_msyfmegt_jkkn3] = React.useState(\"edit\")\n const [sqlIndex_cmp_msyfmegt_jkkn3, setSqlIndex_cmp_msyfmegt_jkkn3] = React.useState(0)\n const [sqlBusy_cmp_msyfmegt_jkkn3, setSqlBusy_cmp_msyfmegt_jkkn3] = React.useState(false)\n const [sqlError_cmp_msyfmegt_jkkn3, setSqlError_cmp_msyfmegt_jkkn3] = React.useState(\"\")\n // Container element, so New can hand the cursor to the first field of the form.\n const sqlHost_cmp_msyfmegt_jkkn3 = React.useRef(null)\n // The Select key can come from the page URL, which is how a detail page reads\n // /api/app/orders/{id} or /api/app/orders?id=… for a single record.\n const sqlUrlKey_cmp_msyfmegt_jkkn3 = readUrlKey(\"query\", sqlKeyParam_cmp_msyfmegt_jkkn3)\n const sqlSelectUrl_cmp_msyfmegt_jkkn3 = React.useMemo(() => {\n if (!sqlselect_cmp_msyfmegt_jkkn3) return \"\"\n const base = sqlUrlKey_cmp_msyfmegt_jkkn3\n ? (() => {\n const bound = bindSqlUrl(sqlselect_cmp_msyfmegt_jkkn3.url, { [sqlKeyParam_cmp_msyfmegt_jkkn3]: sqlUrlKey_cmp_msyfmegt_jkkn3 }, sqlKeyParam_cmp_msyfmegt_jkkn3)\n return bound.keyBound ? bound.url : appendQueryParam(bound.url, sqlKeyParam_cmp_msyfmegt_jkkn3, sqlUrlKey_cmp_msyfmegt_jkkn3)\n })()\n : sqlselect_cmp_msyfmegt_jkkn3.url\n return base\n }, [sqlUrlKey_cmp_msyfmegt_jkkn3])\n // A key in the page URL always narrows the result: a list endpoint ignores the\n // parameter server side, so the requested record is picked out here. Without a\n // key every row is kept and the navigation below takes over.\n const sqlRows_cmp_msyfmegt_jkkn3 = React.useMemo(\n () => filterSqlRowsByKey(toSqlRows(data_source_5w_5ibpv, \"\"), sqlKey_cmp_msyfmegt_jkkn3, sqlUrlKey_cmp_msyfmegt_jkkn3),\n [data_source_5w_5ibpv, sqlUrlKey_cmp_msyfmegt_jkkn3],\n )\n // A fresh result set resets the position; New mode is left untouched so an\n // unsaved draft is not overwritten by a re-render.\n React.useEffect(() => {\n setSqlIndex_cmp_msyfmegt_jkkn3((current) => (current < sqlRows_cmp_msyfmegt_jkkn3.length ? current : 0))\n }, [sqlRows_cmp_msyfmegt_jkkn3])\n React.useEffect(() => {\n const row = sqlRows_cmp_msyfmegt_jkkn3[sqlIndex_cmp_msyfmegt_jkkn3]\n if (!row) return\n setData_cmp_msyfmegt_jkkn3(row)\n setSqlOriginal_cmp_msyfmegt_jkkn3(row)\n setSqlMode_cmp_msyfmegt_jkkn3(\"edit\")\n }, [sqlRows_cmp_msyfmegt_jkkn3, sqlIndex_cmp_msyfmegt_jkkn3])\n const sqlHasKey_cmp_msyfmegt_jkkn3 = (() => {\n const value = readSqlField(data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3)\n return value !== undefined && value !== null && value !== \"\"\n })()\n const sqlSetField_cmp_msyfmegt_jkkn3 = React.useCallback((path, value) => setData_cmp_msyfmegt_jkkn3((current) => setSqlField(current, path, value)), [])\n const sqlReload_cmp_msyfmegt_jkkn3 = React.useCallback(async () => {\n // Without the Read permission the container is not rendered at all; the\n // request is dropped here so it is not fired for a form nobody can see.\n if (!sqlCanSelect_cmp_msyfmegt_jkkn3) return\n if (!sqlSelectUrl_cmp_msyfmegt_jkkn3) return\n // Calling a URL that still holds a placeholder is a guaranteed 400, so the\n // missing key is reported in the component instead.\n if (hasSqlUrlParams(sqlSelectUrl_cmp_msyfmegt_jkkn3)) {\n throw new Error(translate(\"::App.Form.MissingKeyParam\", { 0: sqlKeyParam_cmp_msyfmegt_jkkn3 }))\n }\n const response = await apiService.fetchData({ url: sqlSelectUrl_cmp_msyfmegt_jkkn3, method: \"GET\" })\n setData_source_5w_5ibpv(getByPath(response.data, sqlselect_cmp_msyfmegt_jkkn3.responsePath))\n }, [sqlSelectUrl_cmp_msyfmegt_jkkn3])\n // Reload wrapped with the busy/error handling the toolbar and mount effect need,\n // so a failing Select is reported in the component instead of the console.\n const sqlRefresh_cmp_msyfmegt_jkkn3 = React.useCallback(async () => {\n setSqlBusy_cmp_msyfmegt_jkkn3(true)\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n try {\n await sqlReload_cmp_msyfmegt_jkkn3()\n } catch (error) {\n setSqlError_cmp_msyfmegt_jkkn3(toSqlErrorMessage(error))\n } finally {\n setSqlBusy_cmp_msyfmegt_jkkn3(false)\n }\n }, [sqlReload_cmp_msyfmegt_jkkn3])\n const sqlNew_cmp_msyfmegt_jkkn3 = React.useCallback(() => {\n if (!sqlCanInsert_cmp_msyfmegt_jkkn3) return\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n // Defaults are resolved on every New, so an @today column carries the day the\n // record was opened rather than the day the component was generated.\n const sqlDraft_cmp_msyfmegt_jkkn3 = buildSqlDefaults([{\"path\":\"IsDefault\",\"value\":true},{\"path\":\"IsStatic\",\"value\":true},{\"path\":\"IsPublic\",\"value\":false},{\"path\":\"EntityVersion\",\"value\":3},{\"path\":\"ExtraProperties\",\"value\":\"{}\"}])\n setData_cmp_msyfmegt_jkkn3(sqlDraft_cmp_msyfmegt_jkkn3)\n setSqlOriginal_cmp_msyfmegt_jkkn3({})\n setSqlMode_cmp_msyfmegt_jkkn3(\"new\")\n // After the re-render that paints the empty form, not before it.\n focusFirstSqlField(sqlHost_cmp_msyfmegt_jkkn3)\n }, [])\n const sqlPrev_cmp_msyfmegt_jkkn3 = React.useCallback(() => setSqlIndex_cmp_msyfmegt_jkkn3((current) => Math.max(0, current - 1)), [])\n const sqlNext_cmp_msyfmegt_jkkn3 = React.useCallback(() => setSqlIndex_cmp_msyfmegt_jkkn3((current) => Math.min(sqlRows_cmp_msyfmegt_jkkn3.length - 1, current + 1)), [sqlRows_cmp_msyfmegt_jkkn3])\n const sqlSave_cmp_msyfmegt_jkkn3 = async () => {\n const isNew = sqlMode_cmp_msyfmegt_jkkn3 === \"new\"\n const target = isNew ? sqlinsert_cmp_msyfmegt_jkkn3 : sqlupdate_cmp_msyfmegt_jkkn3\n // The button is already hidden without the permission; a script calling save\n // through a ref must not be able to walk around it either.\n if (!(isNew ? sqlCanInsert_cmp_msyfmegt_jkkn3 : sqlCanUpdate_cmp_msyfmegt_jkkn3)) return\n if (!target) {\n setSqlError_cmp_msyfmegt_jkkn3(isNew ? translate(\"::App.Form.InsertEndpointMissing\") : translate(\"::App.Form.UpdateEndpointMissing\"))\n return\n }\n // Update carries only the edited columns plus the key; Insert sends the record.\n const payload = isNew ? data_cmp_msyfmegt_jkkn3 : toSqlChanges(sqlOriginal_cmp_msyfmegt_jkkn3, data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3)\n if (!isNew && Object.keys(payload).filter((column) => column.toLowerCase() !== String(sqlKey_cmp_msyfmegt_jkkn3).toLowerCase()).length === 0) {\n setSqlError_cmp_msyfmegt_jkkn3(translate(\"::App.Form.NoChanges\"))\n return\n }\n setSqlBusy_cmp_msyfmegt_jkkn3(true)\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n try {\n const response = await callSqlEndpoint(target, data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3, payload)\n // With a Select endpoint the reload is the source of truth and its effect\n // repopulates the form; without one, the response row is all there is.\n if (sqlselect_cmp_msyfmegt_jkkn3) {\n await sqlReload_cmp_msyfmegt_jkkn3()\n } else {\n const saved = toSqlRows(response?.data, \"\")[0]\n if (saved) {\n setData_cmp_msyfmegt_jkkn3(saved)\n setSqlOriginal_cmp_msyfmegt_jkkn3(saved)\n }\n }\n setSqlMode_cmp_msyfmegt_jkkn3(\"edit\")\n } catch (error) {\n setSqlError_cmp_msyfmegt_jkkn3(toSqlErrorMessage(error))\n } finally {\n setSqlBusy_cmp_msyfmegt_jkkn3(false)\n }\n }\n const sqlDelete_cmp_msyfmegt_jkkn3 = async () => {\n if (!sqlCanDelete_cmp_msyfmegt_jkkn3 || !sqldelete_cmp_msyfmegt_jkkn3 || !sqlHasKey_cmp_msyfmegt_jkkn3 || sqlMode_cmp_msyfmegt_jkkn3 === \"new\") return\n // Deleting cannot be undone from the form, so it always asks first.\n const confirmMessage = translate(\"::App.Form.DeleteConfirm\", { 0: sqlKey_cmp_msyfmegt_jkkn3 + \" = \" + readSqlField(data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3) })\n if (typeof window !== \"undefined\" && !window.confirm(confirmMessage)) return\n setSqlBusy_cmp_msyfmegt_jkkn3(true)\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n try {\n await callSqlEndpoint(sqldelete_cmp_msyfmegt_jkkn3, data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3)\n setData_cmp_msyfmegt_jkkn3({})\n setSqlOriginal_cmp_msyfmegt_jkkn3({})\n setSqlMode_cmp_msyfmegt_jkkn3(\"new\")\n await sqlReload_cmp_msyfmegt_jkkn3()\n } catch (error) {\n setSqlError_cmp_msyfmegt_jkkn3(toSqlErrorMessage(error))\n } finally {\n setSqlBusy_cmp_msyfmegt_jkkn3(false)\n }\n }\n React.useEffect(() => { void sqlRefresh_cmp_msyfmegt_jkkn3() }, [sqlRefresh_cmp_msyfmegt_jkkn3])\n\n const filters_source_tq_veb4m = [{ param: \"RoleId\", value: readSqlField(data_cmp_msyfmegt_jkkn3, \"id\"), required: true }]\n const filterKey_source_tq_veb4m = JSON.stringify(filters_source_tq_veb4m)\n React.useEffect(() => {\n const url = buildFilteredUrl(\"/api/app/crudendpoint/AbpUserRoles\", filters_source_tq_veb4m)\n // An unmet required filter leaves the component empty on purpose.\n if (!url) {\n setData_source_tq_veb4m(null)\n return\n }\n let active = true\n apiService.fetchData({ url, method: 'GET' }).then((response) => {\n if (active) setData_source_tq_veb4m(getByPath(response.data, \"\"))\n }).catch((error) => console.error(\"AbpUserRoles · GetList endpoint error:\", error))\n return () => { active = false }\n }, [filterKey_source_tq_veb4m])\n\n React.useEffect(() => {\n let active = true\n apiService.fetchData({ url: \"/api/app/crudendpoint/AbpUsers\", method: 'GET' }).then((response) => {\n if (active) setData_source_gq_0q1dz(getByPath(response.data, \"\"))\n }).catch((error) => console.error(\"AbpUsers · GetList endpoint error:\", error))\n return () => { active = false }\n }, [])\n\n const platformFilters_cmp_mt0288i3_ebpj5 = [{ field: \"Id\", operator: \"=\", kind: \"eq\", value: readSqlField(data_cmp_msyfmegt_jkkn3, \"Id\"), required: true }]\n const platformKey_cmp_mt0288i3_ebpj5 = JSON.stringify(platformFilters_cmp_mt0288i3_ebpj5)\n // The views keep the params object in their own state and write back to it, so\n // it must stay the same object until a filter value actually changes.\n const platformParams_cmp_mt0288i3_ebpj5 = React.useMemo(() => buildPlatformFilterParams(platformFilters_cmp_mt0288i3_ebpj5), [platformKey_cmp_mt0288i3_ebpj5])\n\n const handle_cmp_msyfpfia_ifhdq_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"Name\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyjl707_crcn7_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"NormalizedName\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyjdodn_u3o74_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"ConcurrencyStamp\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyje9ys_2c08a_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"IsDefault\", typeof originalEvent === \"object\" && originalEvent?.target ? Boolean(originalEvent.target.checked) : typeof valueOrEvent === \"object\" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))\n const event = { checked: Boolean(valueOrEvent), originalEvent, target: originalEvent?.target }\n }\n\n const handle_cmp_msyjebu2_27tqi_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"IsStatic\", typeof originalEvent === \"object\" && originalEvent?.target ? Boolean(originalEvent.target.checked) : typeof valueOrEvent === \"object\" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))\n const event = { checked: Boolean(valueOrEvent), originalEvent, target: originalEvent?.target }\n }\n\n const handle_cmp_msyjmetn_gojol_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"IsPublic\", typeof originalEvent === \"object\" && originalEvent?.target ? Boolean(originalEvent.target.checked) : typeof valueOrEvent === \"object\" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))\n const event = { checked: Boolean(valueOrEvent), originalEvent, target: originalEvent?.target }\n }\n\n const handle_cmp_msyjo0g8_oathk_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"EntityVersion\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyjplfq_py8lz_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"ExtraProperties\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n return (\n <>\n {sqlCanSelect_cmp_msyfmegt_jkkn3 ? (\n {sqlHasKey_cmp_msyfmegt_jkkn3 ? sqlKey_cmp_msyfmegt_jkkn3 + \": \" + readSqlField(data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3) : sqlMode_cmp_msyfmegt_jkkn3 === \"new\" ? translate(\"::App.Platform.NewRecord\") : \"\"}}>\n
\n
\n \n \n \n \n {\"Is Default\"}\n \n \n {\"Is Static\"}\n \n \n {\"Is Public\"}\n \n \n \n
\n
\n {sqlRows_cmp_msyfmegt_jkkn3.length > 1 ? (\n <>\n \n {`${sqlIndex_cmp_msyfmegt_jkkn3 + 1} / ${sqlRows_cmp_msyfmegt_jkkn3.length}`}\n \n \n \n ) : null}\n {sqlinsert_cmp_msyfmegt_jkkn3 && sqlCanInsert_cmp_msyfmegt_jkkn3 ? : null}\n {(sqlMode_cmp_msyfmegt_jkkn3 === \"new\" ? sqlinsert_cmp_msyfmegt_jkkn3 && sqlCanInsert_cmp_msyfmegt_jkkn3 : sqlupdate_cmp_msyfmegt_jkkn3 && sqlCanUpdate_cmp_msyfmegt_jkkn3) ? : null}\n {sqldelete_cmp_msyfmegt_jkkn3 && sqlCanDelete_cmp_msyfmegt_jkkn3 ? : null}\n {sqlselect_cmp_msyfmegt_jkkn3 && sqlCanSelect_cmp_msyfmegt_jkkn3 ? : null}\n {sqlMode_cmp_msyfmegt_jkkn3 === \"new\" ? translate(\"::App.Platform.NewRecord\") : translate(\"::App.Platform.Editing\")}\n
\n {sqlError_cmp_msyfmegt_jkkn3 ?
{sqlError_cmp_msyfmegt_jkkn3}
: null}\n
\n
\n ) : null}\n \n
\n {(() => {\n const gridItems_cmp_mt023r5u_rb2jv = (Array.isArray(getByPath(data_source_tq_veb4m, \"\")) ? getByPath(data_source_tq_veb4m, \"\") : Array.isArray(data_source_tq_veb4m) ? data_source_tq_veb4m : null)\n // Nothing loaded yet — an endpoint still in flight, or a required filter\n // holding the request back — is an empty grid, not a binding error.\n if (gridItems_cmp_mt023r5u_rb2jv === null || gridItems_cmp_mt023r5u_rb2jv === undefined) return null\n if (!Array.isArray(gridItems_cmp_mt023r5u_rb2jv)) return
Grid items bağlantısı bir koleksiyon döndürmelidir.
\n const gridColumns_cmp_mt023r5u_rb2jv = [\"UserId\"]\n const gridLookups_cmp_mt023r5u_rb2jv = { \"UserId\": { rows: data_source_gq_0q1dz, path: \"\", value: \"Id\", text: \"UserName\" } }\n if (!gridColumns_cmp_mt023r5u_rb2jv.length) return
Preview için en az bir sütun seçin.
\n return (\n
\n \n \n {gridColumns_cmp_mt023r5u_rb2jv.map((column) => )}\n \n \n {gridItems_cmp_mt023r5u_rb2jv.map((gridItem_cmp_mt023r5u_rb2jv, rowIndex) => (\n \n {gridColumns_cmp_mt023r5u_rb2jv.map((column) => {\n const cellValue_cmp_mt023r5u_rb2jv = column === \"value\" ? gridItem_cmp_mt023r5u_rb2jv : getByPath(gridItem_cmp_mt023r5u_rb2jv, column)\n const lookedUp = resolveLookupText(gridLookups_cmp_mt023r5u_rb2jv[column], cellValue_cmp_mt023r5u_rb2jv)\n const cellText = lookedUp === undefined ? cellValue_cmp_mt023r5u_rb2jv : lookedUp\n const text = typeof cellText === \"object\" && cellText !== null && !(cellText instanceof Date) ? JSON.stringify(cellText) : formatLocaleValue(cellText) || \"—\"\n return \n })}\n \n ))}\n \n
{(column === \"value\" ? \"Value\" : column)}
{text}
\n
\n )\n })()}\n
\n {platformParams_cmp_mt0288i3_ebpj5 === null ? null : (\n \n )}\n \n )\n}\n\nexport default RoleComponent", + "Props": "{\"visualDesigner\":{\"version\":1,\"sourceMode\":\"visual\",\"nodes\":[{\"id\":\"cmp_msyfmegt_jkkn3\",\"type\":\"Form\",\"kind\":\"layout\",\"props\":{\"selectEndpoint\":\"source_5w_5ibpv\",\"insertEndpoint\":\"source_h3_484qa\",\"updateEndpoint\":\"source_gb_d9cvs\",\"deleteEndpoint\":\"source_0t_avok9\",\"keyFieldName\":\"id\",\"collectionPath\":\"\",\"keySource\":\"query\",\"keyParamName\":\"id\",\"previewKeyValue\":\"\",\"autoLoad\":true,\"showToolbar\":true,\"gap\":16,\"className\":\"\",\"title\":\"::AbpIdentity.Roles\",\"columnCount\":2,\"selectPermission\":\"\",\"insertPermission\":\"\"},\"events\":{\"onLoad\":\"\",\"onRecordChange\":\"\",\"onFieldChange\":\"\",\"onNewRecord\":\"\",\"onModeChange\":\"\",\"onBeforeSave\":\"\",\"onAfterSave\":\"\",\"onBeforeDelete\":\"\",\"onAfterDelete\":\"\",\"onError\":\"\"},\"bindings\":{},\"children\":[{\"id\":\"cmp_msyfpfia_ifhdq\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"::App.Platform.Name\",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\",\"sqlDefaultValue\":\"\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"Name\"}},\"children\":[],\"ref\":\"input1\"},{\"id\":\"cmp_msyjl707_crcn7\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Normalized Name :\",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"NormalizedName\"}},\"children\":[],\"ref\":\"input2\"},{\"id\":\"cmp_msyjdodn_u3o74\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Concurrent Stamp : \",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"ConcurrencyStamp\"}},\"children\":[],\"ref\":\"input3\"},{\"id\":\"cmp_msyje9ys_2c08a\",\"type\":\"Checkbox\",\"kind\":\"ui\",\"props\":{\"checked\":false,\"color\":\"blue-500\",\"disabled\":false,\"labelRef\":\"\",\"name\":\"\",\"readOnly\":false,\"value\":\"\",\"field\":null,\"className\":\"\",\"children\":\"Is Default\",\"style\":{},\"sqlDefaultValue\":\"true\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"checked\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"IsDefault\"}},\"children\":[],\"ref\":\"checkbox1\"},{\"id\":\"cmp_msyjebu2_27tqi\",\"type\":\"Checkbox\",\"kind\":\"ui\",\"props\":{\"checked\":false,\"color\":\"blue-500\",\"disabled\":false,\"labelRef\":\"\",\"name\":\"\",\"readOnly\":false,\"value\":\"\",\"field\":null,\"className\":\"\",\"children\":\"Is Static\",\"style\":{},\"sqlDefaultValue\":\"true\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"checked\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"IsStatic\"}},\"children\":[],\"ref\":\"checkbox2\"},{\"id\":\"cmp_msyjmetn_gojol\",\"type\":\"Checkbox\",\"kind\":\"ui\",\"props\":{\"checked\":false,\"color\":\"blue-500\",\"disabled\":false,\"labelRef\":\"\",\"name\":\"\",\"readOnly\":false,\"value\":\"\",\"field\":null,\"className\":\"\",\"children\":\"Is Public\",\"style\":{},\"sqlDefaultValue\":\"false\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"checked\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"IsPublic\"}},\"children\":[],\"ref\":\"checkbox3\"},{\"id\":\"cmp_msyjo0g8_oathk\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Entity Version : \",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"number\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\",\"sqlDefaultValue\":\"3\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"EntityVersion\"}},\"children\":[],\"ref\":\"input4\"},{\"id\":\"cmp_msyjplfq_py8lz\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Extra Properties :\",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\",\"sqlDefaultValue\":\"{}\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"ExtraProperties\"}},\"children\":[],\"ref\":\"input5\"}],\"ref\":\"form1\"},{\"id\":\"cmp_mubctlbp_p6ajt\",\"type\":\"BarcodeScanField\",\"kind\":\"custom\",\"props\":{\"label\":\"\",\"name\":\"barcode\",\"placeholder\":\"\",\"buttonText\":\"\",\"continuous\":true,\"readOnly\":true,\"className\":\"mt-4\"},\"events\":{},\"bindings\":{},\"children\":[],\"ref\":\"barcodeScanField1\"},{\"id\":\"cmp_mt023r5u_rb2jv\",\"type\":\"Grid\",\"kind\":\"ui\",\"props\":{\"hoverable\":true,\"overflow\":true,\"borderlessRow\":false,\"compact\":false,\"cols\":\"3\",\"rows\":\"2\",\"gap\":\"4\",\"colGap\":\"4\",\"rowGap\":\"4\",\"responsive\":true,\"smCols\":\"1\",\"mdCols\":\"2\",\"lgCols\":\"3\",\"xlCols\":\"4\",\"autoFit\":false,\"minColWidth\":\"200px\",\"height\":\"auto\",\"className\":\"mt-4\",\"items\":[{\"id\":\"grid-item-1\",\"content\":\"Grid Item 1\",\"colSpan\":1,\"rowSpan\":1,\"colStart\":1,\"rowStart\":1,\"className\":\"bg-blue-100 p-4 border rounded\"},{\"id\":\"grid-item-2\",\"content\":\"Grid Item 2\",\"colSpan\":1,\"rowSpan\":1,\"colStart\":2,\"rowStart\":1,\"className\":\"bg-green-100 p-4 border rounded\"},{\"id\":\"grid-item-3\",\"content\":\"Grid Item 3\",\"colSpan\":1,\"rowSpan\":1,\"colStart\":3,\"rowStart\":1,\"className\":\"bg-yellow-100 p-4 border rounded\"},{\"id\":\"grid-item-4\",\"content\":\"Grid Item 4\",\"colSpan\":2,\"rowSpan\":1,\"colStart\":1,\"rowStart\":2,\"className\":\"bg-purple-100 p-4 border rounded\"},{\"id\":\"grid-item-5\",\"content\":\"Grid Item 5\",\"colSpan\":1,\"rowSpan\":1,\"colStart\":3,\"rowStart\":2,\"className\":\"bg-red-100 p-4 border rounded\"}],\"dataColumns\":[\"UserId\"],\"columnLookups\":{\"UserId\":{\"sourceId\":\"source_gq_0q1dz\",\"path\":\"\",\"valueField\":\"Id\",\"textField\":\"UserName\"}},\"columnCaptions\":{}},\"events\":{},\"bindings\":{\"items\":{\"sourceId\":\"source_tq_veb4m\",\"path\":\"\"}},\"children\":[],\"ref\":\"grid1\"},{\"id\":\"cmp_mt0288i3_ebpj5\",\"type\":\"ListView\",\"kind\":\"platform\",\"props\":{\"listFormCode\":\"AbpIdentity.Roles\",\"height\":\"520px\",\"filters\":[{\"id\":\"filter_y9_tr73w\",\"field\":\"Id\",\"operator\":\"eq\",\"source\":\"record\",\"value\":\"form1.Id\",\"previewValue\":\"\",\"required\":true}],\"className\":\"mt-4\"},\"events\":{},\"bindings\":{},\"children\":[],\"ref\":\"listView1\"}],\"canvas\":{\"width\":\"responsive\"},\"lifecycle\":{\"onMount\":\"\"},\"dataSources\":[{\"id\":\"source_5w_5ibpv\",\"name\":\"AbpRoles · GetList\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpRoles\",\"responsePath\":\"\"},{\"id\":\"source_i7_msox4\",\"name\":\"AbpRoles · GetById\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpRoles/{id}\",\"responsePath\":\"\"},{\"id\":\"source_0t_avok9\",\"name\":\"AbpRoles · Delete\",\"method\":\"DELETE\",\"url\":\"/api/app/crudendpoint/AbpRoles/{id}\",\"responsePath\":\"\"},{\"id\":\"source_h3_484qa\",\"name\":\"AbpRoles · Create\",\"method\":\"POST\",\"url\":\"/api/app/crudendpoint/AbpRoles\",\"responsePath\":\"\"},{\"id\":\"source_gb_d9cvs\",\"name\":\"AbpRoles · Update\",\"method\":\"PUT\",\"url\":\"/api/app/crudendpoint/AbpRoles/{id}\",\"responsePath\":\"\"},{\"id\":\"source_tq_veb4m\",\"name\":\"AbpUserRoles · GetList\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpUserRoles\",\"responsePath\":\"\",\"filters\":[{\"id\":\"filter_sw_86e0q\",\"field\":\"RoleId\",\"operator\":\"eq\",\"source\":\"record\",\"value\":\"form1.id\",\"previewValue\":\"\",\"required\":true}]},{\"id\":\"source_d2_bri5s\",\"name\":\"AbpUserRoles · GetById\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpUserRoles/{id}\",\"responsePath\":\"\"},{\"id\":\"source_gq_0q1dz\",\"name\":\"AbpUsers · GetList\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpUsers\",\"responsePath\":\"\"},{\"id\":\"source_u8_nu1lu\",\"name\":\"AbpUsers · GetById\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpUsers/{id}\",\"responsePath\":\"\"}],\"permissionCode\":\"\"}}", + "DataSources": "[\r\n {\r\n \"Name\": \"AbpRoles · GetList\",\r\n \"Method\": \"GET\",\r\n \"Url\": \"/api/app/crudendpoint/AbpRoles\",\r\n \"ResponsePath\": null,\r\n \"EntityName\": \"AbpRoles\",\r\n \"OperationType\": \"GetList\",\r\n \"SeedFile\": \"crud/AbpRoles.json\"\r\n },\r\n {\r\n \"Name\": \"AbpRoles · GetById\",\r\n \"Method\": \"GET\",\r\n \"Url\": \"/api/app/crudendpoint/AbpRoles/{id}\",\r\n \"ResponsePath\": null,\r\n \"EntityName\": \"AbpRoles\",\r\n \"OperationType\": \"GetById\",\r\n \"SeedFile\": \"crud/AbpRoles.json\"\r\n },\r\n {\r\n \"Name\": \"AbpRoles · Delete\",\r\n \"Method\": \"DELETE\",\r\n \"Url\": \"/api/app/crudendpoint/AbpRoles/{id}\",\r\n \"ResponsePath\": null,\r\n \"EntityName\": \"AbpRoles\",\r\n \"OperationType\": \"Delete\",\r\n \"SeedFile\": \"crud/AbpRoles.json\"\r\n },\r\n {\r\n \"Name\": \"AbpRoles · Create\",\r\n \"Method\": \"POST\",\r\n \"Url\": \"/api/app/crudendpoint/AbpRoles\",\r\n \"ResponsePath\": null,\r\n \"EntityName\": \"AbpRoles\",\r\n \"OperationType\": \"Create\",\r\n \"SeedFile\": \"crud/AbpRoles.json\"\r\n },\r\n {\r\n \"Name\": \"AbpRoles · Update\",\r\n \"Method\": \"PUT\",\r\n \"Url\": \"/api/app/crudendpoint/AbpRoles/{id}\",\r\n \"ResponsePath\": null,\r\n \"EntityName\": \"AbpRoles\",\r\n \"OperationType\": \"Update\",\r\n \"SeedFile\": \"crud/AbpRoles.json\"\r\n },\r\n {\r\n \"Name\": \"AbpUserRoles · GetList\",\r\n \"Method\": \"GET\",\r\n \"Url\": \"/api/app/crudendpoint/AbpUserRoles\",\r\n \"ResponsePath\": null,\r\n \"EntityName\": \"AbpUserRoles\",\r\n \"OperationType\": \"GetList\",\r\n \"SeedFile\": \"crud/AbpUserRoles.json\"\r\n },\r\n {\r\n \"Name\": \"AbpUserRoles · GetById\",\r\n \"Method\": \"GET\",\r\n \"Url\": \"/api/app/crudendpoint/AbpUserRoles/{id}\",\r\n \"ResponsePath\": null,\r\n \"EntityName\": \"AbpUserRoles\",\r\n \"OperationType\": \"GetById\",\r\n \"SeedFile\": \"crud/AbpUserRoles.json\"\r\n },\r\n {\r\n \"Name\": \"AbpUsers · GetList\",\r\n \"Method\": \"GET\",\r\n \"Url\": \"/api/app/crudendpoint/AbpUsers\",\r\n \"ResponsePath\": null,\r\n \"EntityName\": \"AbpUsers\",\r\n \"OperationType\": \"GetList\",\r\n \"SeedFile\": \"crud/AbpUsers.json\"\r\n },\r\n {\r\n \"Name\": \"AbpUsers · GetById\",\r\n \"Method\": \"GET\",\r\n \"Url\": \"/api/app/crudendpoint/AbpUsers/{id}\",\r\n \"ResponsePath\": null,\r\n \"EntityName\": \"AbpUsers\",\r\n \"OperationType\": \"GetById\",\r\n \"SeedFile\": \"crud/AbpUsers.json\"\r\n }\r\n]" }, { "Id": "8f1c2d40-0000-4000-8000-000000000101", @@ -190,4 +108,4 @@ "DataSources": "" } ] -} +} \ No newline at end of file diff --git a/ui/package-lock.json b/ui/package-lock.json index ba6a8f52..3675d70a 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -36,7 +36,8 @@ "react-modal": "^3.16.3", "react-router-dom": "^7.18.2", "react-select": "^5.10.2", - "yup": "^1.7.1" + "yup": "^1.7.1", + "zxing-wasm": "^3.1.4" }, "devDependencies": { "@eslint/js": "^9.39.5", @@ -3263,6 +3264,12 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/emscripten": { + "version": "1.41.6", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.6.tgz", + "integrity": "sha512-uN+9i8bFT5CUcZfyIEYDrSueACEyKGbUs5kC/72DGlZZoinh84sJfVV0i8UOJD1asdzkvLPBRrKs41kZ8MdEXg==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -11692,6 +11699,18 @@ "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", "license": "MIT" }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tailwindcss": { "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", @@ -12126,6 +12145,21 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.10.0.tgz", + "integrity": "sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -13175,6 +13209,19 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zxing-wasm": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/zxing-wasm/-/zxing-wasm-3.1.4.tgz", + "integrity": "sha512-1+v8p7sauddySVNG+ye7GD/0TS95BG2ycdkrC+EnBrFBFkHeIghzUt2yVjtx3NucJeNrQnhT8yGAIz60Vx9o0w==", + "license": "MIT", + "dependencies": { + "@types/emscripten": "^1.41.6", + "type-fest": "^5.9.0" + }, + "peerDependencies": { + "@types/emscripten": ">=1.39.6" + } } } } diff --git a/ui/package.json b/ui/package.json index 475392f2..12101153 100644 --- a/ui/package.json +++ b/ui/package.json @@ -54,7 +54,8 @@ "react-modal": "^3.16.3", "react-router-dom": "^7.18.2", "react-select": "^5.10.2", - "yup": "^1.7.1" + "yup": "^1.7.1", + "zxing-wasm": "^3.1.4" }, "devDependencies": { "@eslint/js": "^9.39.5", diff --git a/ui/src/components/componentEditor/runtimeNodes.ts b/ui/src/components/componentEditor/runtimeNodes.ts index 4337fb07..e013806a 100644 --- a/ui/src/components/componentEditor/runtimeNodes.ts +++ b/ui/src/components/componentEditor/runtimeNodes.ts @@ -1,5 +1,6 @@ import type React from 'react' import PlatformIcon from './PlatformIcon' +import { BarcodeScanField } from '@/components/shared' import { PublicDemoButton, PublicHeaderBar, @@ -17,6 +18,7 @@ import { */ export const RUNTIME_NODE_COMPONENTS = { PlatformIcon, + BarcodeScanField, PublicHeaderBar, PublicLogo, PublicLanguageSelector, diff --git a/ui/src/components/shared/BarcodeScanner/BarcodeScanField.tsx b/ui/src/components/shared/BarcodeScanner/BarcodeScanField.tsx new file mode 100644 index 00000000..4e669989 --- /dev/null +++ b/ui/src/components/shared/BarcodeScanner/BarcodeScanField.tsx @@ -0,0 +1,83 @@ +import { useState } from 'react' +import { Button, Input } from '@/components/ui' +import { useLocalization } from '@/utils/hooks/useLocalization' +import { isCameraAvailable } from '@/utils/device' +import BarcodeScannerDialog from './BarcodeScannerDialog' + +export type BarcodeScanFieldProps = { + label?: string + placeholder?: string + buttonText?: string + /** Okunan kod formda bu adla taşınır; üretilen kod da aynı adı kullanır. */ + name?: string + /** Açıkken diyalog okumadan sonra kapanmaz, arka arkaya okur. */ + continuous?: boolean + readOnly?: boolean + className?: string + value?: string + onScan?: (code: string) => void +} + +/** + * Tasarımcıdan sürüklenip bırakılabilen barkod alanı. Kanvas ve üretilen kod + * aynı bileşeni çizer; okunan değer hem içeride tutulur hem `onScan` ile dışarı + * verilir, böylece hem tek başına hem bir formun parçası olarak kullanılabilir. + */ +const BarcodeScanField = ({ + label, + placeholder, + buttonText, + name, + continuous = false, + readOnly = true, + className, + value, + onScan, +}: BarcodeScanFieldProps) => { + const { translate } = useLocalization() + const [open, setOpen] = useState(false) + const [scanned, setScanned] = useState('') + const current = value ?? scanned + + const handleDetect = (code: string) => { + setScanned(code) + onScan?.(code) + } + + return ( +
+ {label && } +
+ { + setScanned(event.target.value) + onScan?.(event.target.value) + }} + /> + +
+ + {open && ( + setOpen(false)} + /> + )} +
+ ) +} + +export default BarcodeScanField diff --git a/ui/src/components/shared/BarcodeScanner/BarcodeScannerDialog.tsx b/ui/src/components/shared/BarcodeScanner/BarcodeScannerDialog.tsx new file mode 100644 index 00000000..9503309e --- /dev/null +++ b/ui/src/components/shared/BarcodeScanner/BarcodeScannerDialog.tsx @@ -0,0 +1,111 @@ +import { useState } from 'react' +import { Button, Dialog } from '@/components/ui' +import { useLocalization } from '@/utils/hooks/useLocalization' +import { useBarcodeReader, type BarcodeReaderStatus } from './useBarcodeReader' + +export type BarcodeScannerDialogProps = { + open: boolean + onDialogClose: () => void + /** + * Okunan kod. `false` dönerse kod reddedilmiş sayılır ve listeye yazılmaz; + * sürekli modda yazma hatasını kullanıcıya göstermek için kullanılır. + */ + onDetect: (code: string) => boolean | void | Promise + /** Açıkken diyalog okumadan sonra kapanmaz, okunanları listeler. */ + continuous?: boolean + title?: string +} + +const statusMessageKeys: Record = { + starting: '::App.BarcodeScanner.Starting', + scanning: '::App.BarcodeScanner.Aim', + denied: '::App.BarcodeScanner.PermissionDenied', + insecure: '::App.BarcodeScanner.InsecureContext', + standaloneIos: '::App.BarcodeScanner.StandaloneIos', + error: '::App.BarcodeScanner.CameraError', +} + +/** + * Kamerayla barkod/QR okuma diyaloğu. Tek okuma ve sürekli tarama aynı + * bileşendir; fark yalnızca okumadan sonra kapanıp kapanmadığıdır. + */ +const BarcodeScannerDialog = ({ + open, + onDialogClose, + onDetect, + continuous = false, + title, +}: BarcodeScannerDialogProps) => { + const { translate } = useLocalization() + const [scanned, setScanned] = useState([]) + const [failed, setFailed] = useState() + + const handleDetect = async (code: string) => { + const accepted = await onDetect(code) + if (accepted === false) { + setFailed(code) + return + } + + if (!continuous) { + onDialogClose() + return + } + + setFailed(undefined) + setScanned((previous) => [code, ...previous]) + } + + const { videoRef, status } = useBarcodeReader({ active: open, onDetect: handleDetect }) + + return ( + +
{title ?? translate('::App.BarcodeScanner.Title')}
+ +
+
+ ) +} + +export default BarcodeScannerDialog diff --git a/ui/src/components/shared/BarcodeScanner/BarcodeScannerHost.tsx b/ui/src/components/shared/BarcodeScanner/BarcodeScannerHost.tsx new file mode 100644 index 00000000..bdc81adf --- /dev/null +++ b/ui/src/components/shared/BarcodeScanner/BarcodeScannerHost.tsx @@ -0,0 +1,33 @@ +import { useEffect, useState } from 'react' +import BarcodeScannerDialog from './BarcodeScannerDialog' +import { registerBarcodeScanner } from './scanBarcode' + +/** + * `scanBarcode()` çağrılarını karşılayan tekil diyalog. Uygulama kökünde bir + * kez mount edilir; React ağacı dışından açılabilmesinin tek yolu budur. + */ +const BarcodeScannerHost = () => { + const [resolve, setResolve] = useState<((code: string | null) => void) | null>(null) + + useEffect(() => { + registerBarcodeScanner((next) => setResolve(() => next)) + return () => registerBarcodeScanner(undefined) + }, []) + + const finish = (code: string | null) => { + resolve?.(code) + setResolve(null) + } + + if (!resolve) return null + + return ( + finish(code)} + onDialogClose={() => finish(null)} + /> + ) +} + +export default BarcodeScannerHost diff --git a/ui/src/components/shared/BarcodeScanner/index.ts b/ui/src/components/shared/BarcodeScanner/index.ts new file mode 100644 index 00000000..c56c9cbe --- /dev/null +++ b/ui/src/components/shared/BarcodeScanner/index.ts @@ -0,0 +1,3 @@ +export { default as BarcodeScanField } from './BarcodeScanField' +export { default as BarcodeScannerDialog } from './BarcodeScannerDialog' +export { default as BarcodeScannerHost } from './BarcodeScannerHost' diff --git a/ui/src/components/shared/BarcodeScanner/scanBarcode.ts b/ui/src/components/shared/BarcodeScanner/scanBarcode.ts new file mode 100644 index 00000000..c1229ab7 --- /dev/null +++ b/ui/src/components/shared/BarcodeScanner/scanBarcode.ts @@ -0,0 +1,22 @@ +type ScanRequest = (resolve: (code: string | null) => void) => void + +let requestScan: ScanRequest | undefined + +/** Diyaloğu tutan host bileşen kendini burada kaydeder. */ +export const registerBarcodeScanner = (request?: ScanRequest) => { + requestScan = request +} + +/** + * React ağacı dışından (DevExtreme editör butonu, editorScript) barkod okutur. + * Vazgeçilirse `null` döner. Host mount edilmemişse sessizce `null` döner ki + * çağıran taraf hata yönetmek zorunda kalmasın. + */ +export const scanBarcode = () => + new Promise((resolve) => { + if (!requestScan) { + resolve(null) + return + } + requestScan(resolve) + }) diff --git a/ui/src/components/shared/BarcodeScanner/useBarcodeReader.ts b/ui/src/components/shared/BarcodeScanner/useBarcodeReader.ts new file mode 100644 index 00000000..79bc53f2 --- /dev/null +++ b/ui/src/components/shared/BarcodeScanner/useBarcodeReader.ts @@ -0,0 +1,123 @@ +import { useEffect, useRef, useState } from 'react' +import { + createBarcodeDecoder, + openCameraStream, + stopCameraStream, + vibrateOnScan, +} from '@/utils/barcode' +import { isCameraAvailable, isCameraBlockedByStandaloneIos } from '@/utils/device' + +export type BarcodeReaderStatus = + | 'starting' + | 'scanning' + | 'denied' + | 'insecure' + | 'standaloneIos' + | 'error' + +/** Kare çözme aralığı. Her karede çözmek mobilde pili hızla tüketir. */ +const DECODE_INTERVAL_MS = 180 + +/** Aynı kodun arka arkaya birden çok kez okunmasını engelleyen pencere. */ +const DUPLICATE_WINDOW_MS = 1500 + +/** + * Kamerayı açar, kareleri çözer ve okunan kodu bildirir. + * + * `onDetect` her okumada yeniden bağlanabildiği için ref üzerinden tutulur; + * aksi hâlde her render kamerayı kapatıp yeniden açardı. + */ +export const useBarcodeReader = ({ + active, + onDetect, +}: { + active: boolean + onDetect: (code: string) => void +}) => { + const videoRef = useRef(null) + const onDetectRef = useRef(onDetect) + const [status, setStatus] = useState('starting') + + onDetectRef.current = onDetect + + useEffect(() => { + if (!active) return + + if (isCameraBlockedByStandaloneIos()) { + setStatus('standaloneIos') + return + } + + if (!isCameraAvailable()) { + setStatus('insecure') + return + } + + const video = videoRef.current + let stream: MediaStream | undefined + let timer: number | undefined + let cancelled = false + let lastCode = '' + let lastCodeAt = 0 + + const start = async () => { + setStatus('starting') + try { + const [cameraStream, decode] = await Promise.all([ + openCameraStream(), + createBarcodeDecoder(), + ]) + + if (cancelled) { + stopCameraStream(cameraStream) + return + } + + if (!video) return + stream = cameraStream + + video.srcObject = cameraStream + await video.play() + setStatus('scanning') + + const tick = async () => { + if (cancelled) return + + try { + const code = await decode(video) + const now = Date.now() + if (code && (code !== lastCode || now - lastCodeAt > DUPLICATE_WINDOW_MS)) { + lastCode = code + lastCodeAt = now + vibrateOnScan() + onDetectRef.current(code) + } + } catch { + // Tek bir karenin çözülememesi tarama döngüsünü durdurmaz. + } + + if (!cancelled) { + timer = window.setTimeout(tick, DECODE_INTERVAL_MS) + } + } + + tick() + } catch (error) { + if (cancelled) return + const name = (error as DOMException)?.name + setStatus(name === 'NotAllowedError' || name === 'SecurityError' ? 'denied' : 'error') + } + } + + start() + + return () => { + cancelled = true + if (timer) window.clearTimeout(timer) + stopCameraStream(stream) + if (video) video.srcObject = null + } + }, [active]) + + return { videoRef, status } +} diff --git a/ui/src/components/shared/index.ts b/ui/src/components/shared/index.ts index bcb01091..ec936259 100644 --- a/ui/src/components/shared/index.ts +++ b/ui/src/components/shared/index.ts @@ -2,6 +2,7 @@ export { default as ActionLink } from './ActionLink' export { default as AdaptableCard } from './AdaptableCard' export { default as Affix } from './Affix' export { default as AuthorityCheck } from './AuthorityCheck' +export { BarcodeScanField, BarcodeScannerDialog, BarcodeScannerHost } from './BarcodeScanner' export { default as PermissionCheck } from './PermissionCheck' export { default as ConfirmDialog } from './ConfirmDialog' export { default as Container } from './Container' diff --git a/ui/src/components/visualDesigner/catalog.ts b/ui/src/components/visualDesigner/catalog.ts index 58f2f7ed..8017dcf8 100644 --- a/ui/src/components/visualDesigner/catalog.ts +++ b/ui/src/components/visualDesigner/catalog.ts @@ -110,6 +110,7 @@ const UI_PROPERTY_ALIASES: Record> = { } export const DESIGNER_PRIMARY_EVENTS: Record = { AutoComplete: ['onInputChange', 'onSelect'], + BarcodeScanField: ['onScan'], Button: ['onClick'], Checkbox: ['onChange'], Dropdown: ['onSelect'], @@ -129,6 +130,11 @@ const DESIGNER_EVENT_SNIPPETS: Record> = { // refs.input1.setEnabled(false) // refs.card1.setVisible(false) console.log('Button clicked', event)`, + }, + BarcodeScanField: { + onScan: `// Okunan barkod/QR değeridir (metin). +const code = event +refs.input1.setValue(code)`, }, Checkbox: { onChange: `// Checkbox bileşeninin yeni checked değeri. @@ -1217,6 +1223,35 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [ ], hooks: [], }, + { + // Kamerayla barkod/QR okuyan alan. Kanvas ve uretilen kod ayni bileseni + // cizer (`components/shared/BarcodeScanner`). + name: 'BarcodeScanField', + icon: 'ScanLine', + category: 'basic', + kind: 'custom', + toolboxGroup: 'ui', + description: 'App.CatalogBarcodeScanField.BarcodeScanFieldDescription', + properties: [ + { name: 'label', type: 'string', value: '', category: 'properties' }, + { name: 'name', type: 'string', value: 'barcode', category: 'properties' }, + { name: 'placeholder', type: 'string', value: '', category: 'properties' }, + { name: 'buttonText', type: 'string', value: '', category: 'properties' }, + { name: 'continuous', type: 'boolean', value: false, category: 'properties' }, + { name: 'readOnly', type: 'boolean', value: true, category: 'properties' }, + { name: 'className', type: 'string', value: '', category: 'styling' }, + // `category: 'events'` alanı Özellikler panelinden çıkarıp JavaScript + // sekmesine taşır; orada değer değil script olarak düzenlenir. + { + name: 'onScan', + type: 'function', + value: '', + category: 'events', + description: 'App.CatalogBarcodeScanField.OnScanDescription', + }, + ], + hooks: [], + }, { name: 'Spacer', icon: 'Space', diff --git a/ui/src/proxy/form/models.ts b/ui/src/proxy/form/models.ts index 0cd63506..83f8e0f1 100644 --- a/ui/src/proxy/form/models.ts +++ b/ui/src/proxy/form/models.ts @@ -341,6 +341,8 @@ export interface ImageUploadOptionsDto { maxFileSize?: number width?: number height?: number + /** Mobilde dosya seçici yerine kamerayı açar. */ + capture?: 'environment' | 'user' } export interface RowDto { diff --git a/ui/src/utils/barcode.ts b/ui/src/utils/barcode.ts new file mode 100644 index 00000000..d3bc740e --- /dev/null +++ b/ui/src/utils/barcode.ts @@ -0,0 +1,119 @@ +/** + * Barkod okuma altyapısı. + * + * Tarayıcıda iki çözücü yolu var: Chromium tabanlı tarayıcılar `BarcodeDetector` + * ile yerleşik (WASM indirmeden, donanım hızlandırmalı) okur; iOS Safari ve + * Firefox'ta bu API yok, o yüzden `zxing-wasm` yalnızca gerektiğinde lazy + * yüklenir. Böylece Android cihazlarda ek bir byte inmez. + */ + +/** `BarcodeDetector` format adları (snake_case). 1D + QR. */ +const DETECTOR_FORMATS = [ + 'qr_code', + 'code_128', + 'code_39', + 'code_93', + 'ean_13', + 'ean_8', + 'upc_a', + 'upc_e', + 'itf', +] as const + +/** Aynı küme zxing adlandırmasıyla. İki liste birlikte değişir. */ +const ZXING_FORMATS = [ + 'QRCode', + 'Code128', + 'Code39', + 'Code93', + 'EAN13', + 'EAN8', + 'UPCA', + 'UPCE', + 'ITF', +] as const + +type BarcodeDetectorLike = { + detect: (source: CanvasImageSource) => Promise<{ rawValue: string }[]> +} + +type DetectorWindow = Window & { + BarcodeDetector?: new (options?: { formats?: readonly string[] }) => BarcodeDetectorLike +} + +export type BarcodeDecoder = (frame: HTMLVideoElement) => Promise + +type ZxingReader = (imageData: ImageData) => Promise + +let zxingReader: Promise | undefined + +/** + * zxing-wasm'ı bir kez yükler. WASM dosyası paketten `?url` ile alınır; paketin + * varsayılanı jsDelivr CDN'idir ve çevrimdışı/kapalı ağ kurulumlarında çalışmaz. + */ +const getZxingReader = () => { + zxingReader ??= (async () => { + const [{ prepareZXingModule, readBarcodesFromImageData }, { default: wasmUrl }] = + await Promise.all([ + import('zxing-wasm/reader'), + import('zxing-wasm/reader/zxing_reader.wasm?url'), + ]) + + prepareZXingModule({ overrides: { locateFile: () => wasmUrl } }) + + return async (imageData: ImageData) => { + const results = await readBarcodesFromImageData(imageData, { + formats: [...ZXING_FORMATS], + tryHarder: false, + }) + return results.find((result) => result.text)?.text + } + })() + return zxingReader +} + +/** + * Ortama göre çözücü seçer. Çağıran taraf hangi yolun kullanıldığını bilmez; + * her iki yol da video karesini alıp okunan metni döner. + */ +export const createBarcodeDecoder = async (): Promise => { + const DetectorCtor = (window as DetectorWindow).BarcodeDetector + + if (DetectorCtor) { + const detector = new DetectorCtor({ formats: DETECTOR_FORMATS }) + return async (frame) => { + const codes = await detector.detect(frame) + return codes.find((code) => code.rawValue)?.rawValue + } + } + + const read = await getZxingReader() + // WASM yolunda kare canvas'a indirgenir; canvas tek kez oluşturulup yeniden kullanılır. + const canvas = document.createElement('canvas') + const context = canvas.getContext('2d', { willReadFrequently: true }) + + return async (frame) => { + if (!context || !frame.videoWidth || !frame.videoHeight) return undefined + canvas.width = frame.videoWidth + canvas.height = frame.videoHeight + context.drawImage(frame, 0, 0) + return read(context.getImageData(0, 0, canvas.width, canvas.height)) + } +} + +/** Arka kamerayı tercih eden akış. İzin reddedilirse hata çağırana bırakılır. */ +export const openCameraStream = () => + navigator.mediaDevices.getUserMedia({ + video: { + facingMode: { ideal: 'environment' }, + width: { ideal: 1280 }, + height: { ideal: 720 }, + }, + audio: false, + }) + +export const stopCameraStream = (stream?: MediaStream | null) => + stream?.getTracks().forEach((track) => track.stop()) + +/** Okuma geri bildirimi. Desteklemeyen cihazda sessizce atlanır. */ +export const vibrateOnScan = () => navigator.vibrate?.(60) diff --git a/ui/src/utils/device.ts b/ui/src/utils/device.ts new file mode 100644 index 00000000..6749d882 --- /dev/null +++ b/ui/src/utils/device.ts @@ -0,0 +1,35 @@ +/** + * Cihaz yetenekleri. Kamera/konum gibi özellikler tarayıcıya ve bağlama göre + * değiştiği için tek yerden sorulur; ekranlar `navigator` kontrolü yapmaz. + */ + +/** Kamera güvenli bağlam ister; HTTP üzerinden getUserMedia hiç tanımlı değildir. */ +export const isCameraAvailable = () => + typeof window !== 'undefined' && window.isSecureContext && !!navigator.mediaDevices?.getUserMedia + +/** + * iOS'ta ana ekrana eklenmiş PWA içinde getUserMedia yalnızca 16.4+ sürümlerde + * çalışır; altındaki sürümlerde kamera sessizce boş akış döner. Kullanıcıya + * "Safari'de aç" diyebilmek için ayrıca tespit ediyoruz. + */ +export const isCameraBlockedByStandaloneIos = () => { + if (typeof navigator === 'undefined') return false + if (!/iPad|iPhone|iPod/.test(navigator.userAgent)) return false + if ((navigator as Navigator & { standalone?: boolean }).standalone !== true) return false + + const version = /OS (\d+)_(\d+)/.exec(navigator.userAgent) + if (!version) return false + const [major, minor] = [Number(version[1]), Number(version[2])] + return major < 16 || (major === 16 && minor < 4) +} + +/** + * `` yalnızca dokunmatik cihazlarda kamerayı açar; masaüstünde + * sessizce yok sayılır ve aynı dosya seçicisini ikinci kez açan bir buton + * bırakırdı. Bu yüzden kamera butonu yalnızca kaba işaretçili (parmak) ve + * arka kamerası olması beklenen cihazlarda gösterilir. + */ +export const supportsPhotoCapture = () => + typeof window !== 'undefined' && + typeof window.matchMedia === 'function' && + window.matchMedia('(pointer: coarse)').matches diff --git a/ui/src/utils/editorScriptRuntime.ts b/ui/src/utils/editorScriptRuntime.ts index 0cdf1243..97453d5e 100644 --- a/ui/src/utils/editorScriptRuntime.ts +++ b/ui/src/utils/editorScriptRuntime.ts @@ -13,6 +13,9 @@ * yeniden derlenir. */ +import { getLocalization } from '@/services/localization.service' +import { store } from '@/store' + export type EditorScriptRuntimeContext = { formData: Record e: any @@ -56,6 +59,7 @@ const HELPER_PARAMS = [ 'ask', 'openUrl', 'api', + 'geoTo', 'revert', ] @@ -63,6 +67,12 @@ const HELPER_PARAMS = [ * Ortak yardımcılar * ------------------------------------------------------------------ */ +/** Script runtime React ağacının dışındadır; dil metni store'dan okunur. */ +const translateRuntime = (key: string) => { + const { texts, config } = store.getState().abpConfig + return getLocalization(texts ?? {}, config?.localization?.defaultResourceName, key) +} + /** Alan adını büyük/küçük harf duyarsız arar; SQL kolonları tutarsız gelebiliyor. */ const resolveKey = (source: any, name: string): string | undefined => { if (!source || typeof source !== 'object') return undefined @@ -243,6 +253,36 @@ const buildHelpers = ({ return responsePath ? pickPath(json, responsePath) : json } + /** + * Cihazın konumunu alanlara yazar. Tek atıştır; izin verilmezse ya da konum + * alınamazsa alanlara dokunulmaz ve kullanıcıya mesaj gösterilir — sessizce + * boş değer yazmak, kullanıcının konumu kaydettiğini sanmasına yol açardı. + */ + const geoTo = async (latField: string, lngField?: string, accuracyField?: string) => { + if (!navigator.geolocation) { + notify(translateRuntime('::App.Geolocation.Unsupported')) + return false + } + + try { + const position = await new Promise((resolve, reject) => { + navigator.geolocation.getCurrentPosition(resolve, reject, { + enableHighAccuracy: true, + timeout: 10000, + maximumAge: 0, + }) + }) + + set(latField, position.coords.latitude) + if (lngField) set(lngField, position.coords.longitude) + if (accuracyField) set(accuracyField, Math.round(position.coords.accuracy)) + return true + } catch { + notify(translateRuntime('::App.Geolocation.Failed')) + return false + } + } + const helpers = { value: e?.value, field, @@ -270,6 +310,7 @@ const buildHelpers = ({ ask, openUrl, api, + geoTo, revert, } @@ -304,6 +345,7 @@ const buildHelpers = ({ helpers.ask, helpers.openUrl, helpers.api, + helpers.geoTo, helpers.revert, ], } diff --git a/ui/src/views/Views.tsx b/ui/src/views/Views.tsx index f19afdc6..fc5773ab 100644 --- a/ui/src/views/Views.tsx +++ b/ui/src/views/Views.tsx @@ -1,4 +1,5 @@ import type { LayoutType } from '@/proxy/theme/models' +import { BarcodeScannerHost } from '@/components/shared' import Loading from '@/components/shared/Loading' import { Alert, Button } from '@/components/ui' import { store, useStoreState } from '@/store' @@ -89,6 +90,7 @@ const Views = (props: ViewsProps) => { {e.cid} ))} + diff --git a/ui/src/views/admin/listForm/edit/json-row-operations/editor-options/optionSpecs.ts b/ui/src/views/admin/listForm/edit/json-row-operations/editor-options/optionSpecs.ts index f9ea3eee..41b676fe 100644 --- a/ui/src/views/admin/listForm/edit/json-row-operations/editor-options/optionSpecs.ts +++ b/ui/src/views/admin/listForm/edit/json-row-operations/editor-options/optionSpecs.ts @@ -782,6 +782,19 @@ export const optionSpecs: OptionSpec[] = [ platform: true, placeholder: 'image/*', }, + { + path: 'capture', + label: 'capture', + type: 'select', + group: 'image', + editors: IMAGE_EDITORS, + platform: true, + help: 'App.SpecCapture.Help', + choices: [ + { value: 'environment', label: 'App.SpecCapture.Environment' }, + { value: 'user', label: 'App.SpecCapture.User' }, + ], + }, { path: 'multiple', label: 'multiple', diff --git a/ui/src/views/admin/listForm/edit/json-row-operations/editor-options/presets.ts b/ui/src/views/admin/listForm/edit/json-row-operations/editor-options/presets.ts index 056894e6..7d950752 100644 --- a/ui/src/views/admin/listForm/edit/json-row-operations/editor-options/presets.ts +++ b/ui/src/views/admin/listForm/edit/json-row-operations/editor-options/presets.ts @@ -264,6 +264,29 @@ export const editorOptionsPresets: EditorOptionsPreset[] = [ editors: ['dxHtmlEditor'], value: buildHtmlEditorOptions(), }, + { + key: 'photoCapture', + label: 'App.PresetPhotoCapture.PhotoCaptureLabel', + description: 'App.PresetPhotoCapture.PhotoCaptureDescription', + editors: ['dxImageUpload'], + value: { accept: 'image/*', capture: 'environment' }, + }, + { + key: 'barcodeScan', + label: 'App.PresetBarcodeScan.BarcodeScanLabel', + description: 'App.PresetBarcodeScan.BarcodeScanDescription', + editors: ['dxTextBox'], + value: { + buttons: [ + { + name: 'scanBarcode', + location: 'after', + // '@SCAN_BARCODE' token'ini editingForm.ts kamera diyaloguna baglar. + options: { icon: 'photo', stylingMode: 'text', onClick: '@SCAN_BARCODE' }, + }, + ], + }, + }, { key: 'height100', label: 'App.PresetHeight100.Height100Label', diff --git a/ui/src/views/admin/listForm/edit/json-row-operations/editor-script/scriptRecipes.ts b/ui/src/views/admin/listForm/edit/json-row-operations/editor-script/scriptRecipes.ts index 8879f6ef..e32e768e 100644 --- a/ui/src/views/admin/listForm/edit/json-row-operations/editor-script/scriptRecipes.ts +++ b/ui/src/views/admin/listForm/edit/json-row-operations/editor-script/scriptRecipes.ts @@ -118,6 +118,7 @@ export type RecipeId = | 'ask' | 'openUrl' | 'apiToField' + | 'location' | 'custom' export const recipeGroups: { key: RecipeGroupKey; title: string; description: string }[] = [ @@ -667,6 +668,33 @@ export const recipes: Recipe[] = [ target: label(params.target, t('::App.Listform.ListformField.Target')), }), }, + { + id: 'location', + label: 'App.ScriptBuilderLocation.LocationLabel', + group: 'integration', + summary: 'App.ScriptBuilderLocation.LocationSummary', + example: "await geoTo('Enlem', 'Boylam')", + params: [ + { key: 'lat', label: 'App.ScriptBuilderLocation.LatLabel', type: 'field' }, + { key: 'lng', label: 'App.ScriptBuilderLocation.LngLabel', type: 'field' }, + { + key: 'accuracy', + label: 'App.ScriptBuilderLocation.AccuracyLabel', + type: 'field', + optional: true, + help: 'App.ScriptBuilderLocation.AccuracyHelp', + }, + ], + build: ({ params }) => + `await geoTo(${q(params.lat)}, ${q(params.lng)}${ + params.accuracy ? `, ${q(params.accuracy)}` : '' + })`, + describe: ({ params }, t) => + t('::App.ScriptBuilderDescribe.Location', { + lat: label(params.lat, t('::App.ScriptBuilderLocation.LatLabel')), + lng: label(params.lng, t('::App.ScriptBuilderLocation.LngLabel')), + }), + }, { id: 'custom', label: 'App.ScriptBuilderCustom.CustomLabel', diff --git a/ui/src/views/developerKit/VisualComponentDesigner.tsx b/ui/src/views/developerKit/VisualComponentDesigner.tsx index 6c282ac5..67e1b99e 100644 --- a/ui/src/views/developerKit/VisualComponentDesigner.tsx +++ b/ui/src/views/developerKit/VisualComponentDesigner.tsx @@ -5732,6 +5732,13 @@ const VisualComponentDesigner = () => { {target.node.type} + {/* Aynı tipten birden çok bileşen listede ayırt edilemiyordu; + ref adı script içinde kullanılan addır. */} + {target.node.ref && ( + + ({target.node.ref}) + + )} {target.eventNames.length} diff --git a/ui/src/views/form/editors/ImageUploadEditorComponent.tsx b/ui/src/views/form/editors/ImageUploadEditorComponent.tsx index 8e289345..90ccbc21 100644 --- a/ui/src/views/form/editors/ImageUploadEditorComponent.tsx +++ b/ui/src/views/form/editors/ImageUploadEditorComponent.tsx @@ -1,6 +1,6 @@ import { ImageUploadOptionsDto } from '@/proxy/form/models' import { ReactElement, useEffect, useMemo, useRef, useState } from 'react' -import { FaSpinner, FaUpload } from 'react-icons/fa' +import { FaCamera, FaSpinner, FaUpload } from 'react-icons/fa' import { Button } from '@/components/ui' import { useLocalization } from '@/utils/hooks/useLocalization' import { @@ -10,6 +10,7 @@ import { } from './imageHoverPreview' import { normalizeImageSize } from '../../list/shared/imageValue' import Input from '@/components/ui/Input' +import { supportsPhotoCapture } from '@/utils/device' const ImageUploadEditorComponent = ({ value, @@ -34,6 +35,9 @@ const ImageUploadEditorComponent = ({ const thumbW = normalizeImageSize(resolvedOptions.width, 40) const thumbH = normalizeImageSize(resolvedOptions.height, 40) + // Alanda `capture` acikca ayarliysa kullanicinin istedigi tek davranis odur; + // aksi halde dokunmatik cihazda dosya seciciye ek olarak kamera sunulur. + const showCameraButton = !resolvedOptions.capture && supportsPhotoCapture() const valueUrls = useMemo( () => (Array.isArray(value) ? value : [value]).filter(Boolean) as string[], @@ -43,6 +47,7 @@ const ImageUploadEditorComponent = ({ const [urls, setUrls] = useState(valueUrls) const [uploading, setUploading] = useState(false) const inputRef = useRef(null) + const cameraInputRef = useRef(null) // Değer dışarıdan (editör script'i, veri yenileme…) değiştiğinde önizlemeler // güncellenmiyordu; state yalnızca ilk render'da kuruluyordu. @@ -98,6 +103,8 @@ const ImageUploadEditorComponent = ({ } finally { setUploading(false) if (inputRef.current) inputRef.current.value = '' + // Ayni fotografi ikinci kez cekmek degisiklik olayini tetiklemezdi. + if (cameraInputRef.current) cameraInputRef.current.value = '' } } @@ -159,13 +166,27 @@ const ImageUploadEditorComponent = ({ ref={inputRef} type="file" accept={resolvedOptions.accept ?? 'image/*'} + capture={resolvedOptions.capture} multiple={isMultiple} style={{ display: 'none' }} onChange={(e) => handleFiles(e.target.files)} /> + {/* Kamera ayri bir input'tur: `capture` dosya seciciyi kapattigi icin + tek input ile hem galeri hem kamera sunulamaz. */} + {showCameraButton && ( + handleFiles(e.target.files)} + /> + )} + {showCameraButton && ( + + )} )} diff --git a/ui/src/views/list/editors/ImageUploadEditorComponent.tsx b/ui/src/views/list/editors/ImageUploadEditorComponent.tsx index dbf08b43..d15c0908 100644 --- a/ui/src/views/list/editors/ImageUploadEditorComponent.tsx +++ b/ui/src/views/list/editors/ImageUploadEditorComponent.tsx @@ -1,6 +1,6 @@ import type { ReactElement } from 'react' import { useEffect, useMemo, useRef, useState } from 'react' -import { FaSpinner, FaUpload } from 'react-icons/fa' +import { FaCamera, FaSpinner, FaUpload } from 'react-icons/fa' import { Button } from '@/components/ui' import { useLocalization } from '@/utils/hooks/useLocalization' import { @@ -10,9 +10,12 @@ import { } 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 @@ -79,6 +82,7 @@ const ImageUploadEditorComponent = (cellElement: any): ReactElement => { const col = cellElement.column const [uploading, setUploading] = useState(false) const inputRef = useRef(null) + const cameraInputRef = useRef(null) const [urls, setUrls] = useState(() => normalizeUploadValue(cellElement.value)) useEffect(() => { @@ -96,6 +100,9 @@ const ImageUploadEditorComponent = (cellElement: any): ReactElement => { 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) @@ -141,6 +148,8 @@ const ImageUploadEditorComponent = (cellElement: any): ReactElement => { } finally { setUploading(false) if (inputRef.current) inputRef.current.value = '' + // Ayni fotografi ikinci kez cekmek degisiklik olayini tetiklemezdi. + if (cameraInputRef.current) cameraInputRef.current.value = '' } } @@ -198,13 +207,27 @@ const ImageUploadEditorComponent = (cellElement: any): ReactElement => { 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 && ( + handleFiles(e.target.files)} + /> + )} + {showCameraButton && ( + + )} ) } diff --git a/ui/src/views/list/shared/editingForm.ts b/ui/src/views/list/shared/editingForm.ts index 69068ff4..329e06e3 100644 --- a/ui/src/views/list/shared/editingForm.ts +++ b/ui/src/views/list/shared/editingForm.ts @@ -13,6 +13,7 @@ import { GridDto, PlatformEditorTypes, } from '@/proxy/form/models' +import { scanBarcode } from '@/components/shared/BarcodeScanner/scanBarcode' import { getNextSequenceValue } from '@/services/form.service' import type { EditorType2, RowMode, SimpleItemWithColData } from '../../form/types' import { @@ -26,6 +27,21 @@ import { export type EditorOptionsWithButtons = { buttons?: any[] } & Record +/** + * `editorOptions.buttons` içinde bu değeri taşıyan `onClick`, kamerayla barkod + * okuyup değeri alana yazar. Token kullanılmasının nedeni, buton tanımının + * veritabanında JSON olarak durması ve oradan fonksiyon taşınamamasıdır. + */ +const SCAN_BARCODE_TOKEN = '@SCAN_BARCODE' + +/** Token'ın arkasındaki gerçek davranış. Okuma iptal edilirse alana dokunulmaz. */ +const scanBarcodeIntoEditor = async (e: any) => { + const code = await scanBarcode() + if (code !== null) { + e?.component?.option('value', code) + } +} + /* ------------------------------------------------------------------ * * Satır anahtarı yardımcıları * ------------------------------------------------------------------ */ @@ -476,7 +492,9 @@ export const createFormItemMapper = ({ if (editorOptions.buttons) { editorOptions.buttons = editorOptions.buttons.map((btn: any) => { - if (typeof btn?.options?.onClick === 'string') { + 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})`) } diff --git a/ui/src/views/list/useToolbar.tsx b/ui/src/views/list/useToolbar.tsx index 84c72c6e..f05df674 100644 --- a/ui/src/views/list/useToolbar.tsx +++ b/ui/src/views/list/useToolbar.tsx @@ -15,6 +15,10 @@ import { workflowService } from '@/services/workflow.service' import type { WorkflowRunResultDto } from '@/services/workflow.service' import { splitWorkflowApprovers, translateWorkflowLabel } from '@/utils/workflow/workflowHelpers' import { safeJsonParse } from './Utils' +import { BarcodeScannerDialog } from '@/components/shared' + +/** Toolbar butonunun `dialogName` degeri; surekli barkod taramasini acar. */ +const BARCODE_SCAN_DIALOG = 'BarcodeScanDialog' type ToolbarModalData = { open: boolean @@ -87,6 +91,56 @@ const useToolbar = ({ const [toolbarModalData, setToolbarModalData] = useState() const grdOpt = gridDto?.gridOptions + /** + * Surekli barkod taramasi. Okunan her kod listeye yeni bir satir olarak + * yazilir; ekranin kendi insert ucundan gectigi icin kiracin izolasyonu ve + * yetki kontrolu normal satir eklemeyle aynidir. + */ + const openBarcodeScan = (action: { dialogParameters?: string }) => { + const parameters = safeJsonParse>(action.dialogParameters, {}) + const targetField = parameters.field + const insertUrl = grdOpt?.insertServiceAddress + + if (!targetField || !insertUrl) { + toast.push( + + {translate('::App.BarcodeScanner.NotConfigured')} + , + { placement: 'bottom-end' }, + ) + return + } + + // Barkod disindaki sabit alanlar (durum, depo vb.) dialogParameters'tan gelir. + const { field: _field, ...defaults } = parameters + + setToolbarModalData({ + open: true, + content: ( + { + try { + await dynamicFetch(insertUrl, 'POST', undefined, { + data: { ...defaults, [targetField]: code }, + listFormCode, + }) + return true + } catch { + // Diyalog acik kalir; kullanici kodu tekrar okutabilir. + return false + } + }} + onDialogClose={() => { + setToolbarModalData(undefined) + refreshData() + }} + /> + ), + }) + } + function getToolbarData() { const items: ToolbarItem[] = [] @@ -673,7 +727,9 @@ const useToolbar = ({ if (typeof e.event?.preventDefault === 'function') { e?.event?.preventDefault() } - if (action.url) { + if (action.dialogName === BARCODE_SCAN_DIALOG) { + openBarcodeScan(action) + } else if (action.url) { let url = action.url // griddeki secili satirlari al const selectedRowsData = getSelectedRowsData()