MultiValueStringJsonConverter
This commit is contained in:
parent
c4de55d459
commit
78bc20b645
10 changed files with 150 additions and 11 deletions
6
.github/instructions/dotnet.instructions.md
vendored
6
.github/instructions/dotnet.instructions.md
vendored
|
|
@ -118,6 +118,12 @@ Ek kurallar:
|
|||
uyarı bırakılmaz.
|
||||
- **Localization**: kullanıcıya görünen metin koda gömülmez, `PlatformResource`
|
||||
anahtarı üzerinden verilir.
|
||||
- **Çok değerli alan taşıyan girdi DTO'su**: ekranda `dxTagBox` (ya da çoklu seçimli
|
||||
`dxGridBox`) ile doldurulan bir alan tipli bir DTO'ya bağlanıyorsa property `string`
|
||||
kalır ve `[JsonConverter(typeof(MultiValueStringJsonConverter))]` ile işaretlenir —
|
||||
UI diziyi olduğu gibi yollar, converter `PlatformConsts.MultiValueDelimiter` ile tek
|
||||
string üretir. Varsayılan `list-form-data/*` ucundan geçen ekranlarda bu iş
|
||||
`QueryHelper` tarafından yapılır, DTO yoktur; okuma DTO'larına öznitelik eklenmez.
|
||||
- **Sabitler**: bağlantı dizesi, tenant id, anahtar/secret koda yazılmaz; konfigürasyon
|
||||
veya `SettingDefinition` üzerinden okunur.
|
||||
|
||||
|
|
|
|||
|
|
@ -309,6 +309,16 @@ komuta parametre olarak katılır (`lowcode` §4.3.5). **String birleştirme ile
|
|||
Service'e devretmek istiyorsan bu alanı o ucun adresiyle değiştirirsin — ekranın geri kalanı
|
||||
(form, doğrulama, yetki) aynı kalır.
|
||||
|
||||
**Çok değerli alanlar bu iki yolda farklı davranır.** Varsayılan uçta payload bir sözlüktür
|
||||
(`data: {...}`) ve `dxTagBox` dizisini `QueryHelper.GetFormattedValue` `|` ile birleştirir.
|
||||
Tipli bir DTO alan uçta (`list-form-dynamic-api/...` gibi) böyle bir adım yoktur: dizi `string`
|
||||
property'ye bağlanamaz ve istek `400` ile düşer. Çözüm, DTO alanına
|
||||
`[JsonConverter(typeof(MultiValueStringJsonConverter))]` eklemektir — `Modules` alanında
|
||||
uygulanmış örneği `CreateUpdateTenantInput` / `CreateTenantFromOrderInput` taşır. Yalnızca
|
||||
**girdi** DTO'larına eklenir; okuma DTO'larında (`SettingDefinitionDto.Providers`,
|
||||
`PlatformPermissionGrantInfoDto.Modules`) karşılığı yoktur çünkü o ekranlar varsayılan SQL
|
||||
yolundan kaydeder.
|
||||
|
||||
---
|
||||
|
||||
## 5. Ekran seviyesindeki diğer bloklar
|
||||
|
|
@ -755,7 +765,7 @@ sessizce yok sayılır:
|
|||
| DTO | Hangi `EditorType2` | Anahtarlar (varsayılan) |
|
||||
| --- | --- | --- |
|
||||
| `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`) |
|
||||
| `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 |
|
||||
|
||||
Çözüm **yalnızca alanın `EditorType2` değeri eşleşiyorsa** yapılır: `dxTextBox` bir alana
|
||||
|
|
|
|||
24
.github/instructions/lowcode.instructions.md
vendored
24
.github/instructions/lowcode.instructions.md
vendored
|
|
@ -738,7 +738,7 @@ anahtarları listeler; ortak anahtarlar (§4.3.3) her editörde geçerlidir.
|
|||
| `dxLookup` | Tek seçim (çok kayıt, mobil dostu) | FK tipi | `dropDownOptions.*`, `searchEnabled`, `applyValueMode` + **lookup zorunlu** |
|
||||
| `dxDropDownBox` | Özel içerikli açılır kutu | FK tipi | `dropDownOptions.width/height/hideOnOutsideClick`, `deferRendering`, `openOnFieldClick` |
|
||||
| `dxGridBox` | Çok kolonlu seçim ızgarası | FK tipi | `columns`\*, `filterRowVisible`\*, `selectionMode`\*, `acceptCustomValue`\* + **lookup zorunlu** |
|
||||
| `dxTagBox` | Çok değerli seçim | String (virgüllü) / ilişki tablosu | `showSelectionControls`\*, `maxDisplayedTags`\*, `showMultiTagOnly`\*, `applyValueMode`\*, `searchEnabled`\*, `acceptCustomValue`\*, `hideSelectedItems`, `multiline` + **lookup zorunlu** |
|
||||
| `dxTagBox` | Çok değerli seçim | String (`\|` ile ayrılmış — §4.3.2.1) | `showSelectionControls`\*, `maxDisplayedTags`\*, `showMultiTagOnly`\*, `applyValueMode`\*, `searchEnabled`\*, `acceptCustomValue`\*, `hideSelectedItems`, `multiline` + **lookup zorunlu** |
|
||||
| `dxAutocomplete` | Serbest metin + öneri | String (16) | `minSearchLength`, `searchExpr`, `searchTimeout` |
|
||||
| `dxColorBox` | Renk | String (16) | `editAlphaChannel`, `keyStep` |
|
||||
| `dxSlider` | Tek değerli kaydırıcı | Sayı | `min`, `max`, `tooltip.showMode` |
|
||||
|
|
@ -768,6 +768,28 @@ sayı, `multiple` bir boolean olmalıdır.
|
|||
`3` (WebService) ise noktalı virgülle ayrılmış bir çağrı tarifidir. Tam sözleşme ve cascade
|
||||
(ebeveyn–çocuk) kurulumu için §4.3.4.
|
||||
|
||||
### 4.3.2.1 Çok değerli alanların depolama sözleşmesi
|
||||
|
||||
`dxTagBox` (ve `selectionMode: "multiple"` verilmiş `dxGridBox`) tek bir **metin kolonunda**,
|
||||
değerleri `|` ile ayırarak saklar (`PlatformConsts.MultiValueDelimiter`). İlişki tablosu
|
||||
açmaya gerek yoktur; kolon `NVARCHAR(n)` olarak tanımlanır ve `DbSourceType: 16` verilir.
|
||||
|
||||
Zincirin iki yönü **farklı yerlerde** kapanır — yeni ekran üretirken üçünü birden kontrol et:
|
||||
|
||||
| Yön | Nerede olur | Not |
|
||||
| --- | --- | --- |
|
||||
| Okuma (`"A\|B"` → `["A","B"]`) | UI, `useListFormCustomDataSource` | Alanın `EditorType2` değeri `dxTagBox` ise kolon `extras.multiValue` alır; ayrıştırma buna bakar |
|
||||
| Yazma, **SQL yolu** (`["A","B"]` → `"A\|B"`) | Backend, `QueryHelper.GetFormattedValue` | Yalnızca alanın `DbSourceType`'ı String (16) ise birleştirir — sayısal tip verirsen dizi sessizce bozulur |
|
||||
| Yazma, **tipli DTO yolu** | Yok — elle kurulur | Aşağıya bak |
|
||||
|
||||
**Tuzak:** ekranın `*ServiceAddress` alanı varsayılan `list-form-data/*` yerine tipli bir DTO
|
||||
alan bir uca (`list-form-dynamic-api/...`, Custom Endpoint, Dynamic Service) bakıyorsa
|
||||
`QueryHelper` devreye girmez; gelen dizi `string` property'ye bağlanamaz ve istek
|
||||
`400 Bad Request` + `"The JSON value could not be converted to System.String"` ile düşer.
|
||||
Bu durumda DTO'daki alana `[JsonConverter(typeof(MultiValueStringJsonConverter))]` eklenir
|
||||
(`Application.Contracts/ListForms/DynamicApi/`); converter hem diziyi hem düz metni kabul eder
|
||||
ve `|` ile birleştirilmiş tek string üretir. Salt okuma DTO'larına eklenmez.
|
||||
|
||||
### 4.3.3 Her editörde geçerli ortak seçenekler
|
||||
|
||||
Aşağıda en sık kullanılanlar özetlenir; **tam sözlük ve grup grup liste §6.3'tedir** —
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Sozsoft.Platform.ListForms.DynamicApi;
|
||||
|
||||
|
|
@ -6,5 +7,9 @@ public class CreateTenantFromOrderInput
|
|||
{
|
||||
public Guid OrderId { get; set; }
|
||||
public string Name { get; set; }
|
||||
/// <summary>
|
||||
/// Cok degerli alan: dizi de duz metin de gelebilir (<see cref="MultiValueStringJsonConverter"/>).
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(MultiValueStringJsonConverter))]
|
||||
public string Modules { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Sozsoft.Platform.ListForms.DynamicApi;
|
||||
|
||||
|
|
@ -27,6 +28,11 @@ public class CreateUpdateTenantInput
|
|||
public string FaxNumber { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Website { get; set; }
|
||||
/// <summary>
|
||||
/// Form tarafinda dxTagBox oldugu icin dizi de gelebilir; converter pipe ile
|
||||
/// ayrilmis tek string'e cevirir (<see cref="MultiValueStringJsonConverter"/>).
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(MultiValueStringJsonConverter))]
|
||||
public string Modules { get; set; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Sozsoft.Platform.ListForms.DynamicApi;
|
||||
|
||||
/// <summary>
|
||||
/// Cok degerli editorler (dxTagBox) formda deger olarak dizi tasir; SQL yolunda diziyi
|
||||
/// <c>QueryHelper</c> pipe ile birlestirir, tipli DTO tasiyan dynamic API uclarinda ise
|
||||
/// boyle bir adim yoktur. Bu donusturucu ayni sozlesmeyi DTO tarafinda kurar: gelen deger
|
||||
/// dizi de olsa duz metin de olsa pipe ile ayrilmis tek string uretir.
|
||||
/// </summary>
|
||||
public class MultiValueStringJsonConverter : JsonConverter<string>
|
||||
{
|
||||
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (reader.TokenType != JsonTokenType.StartArray)
|
||||
{
|
||||
return reader.GetString();
|
||||
}
|
||||
|
||||
var values = new List<string>();
|
||||
|
||||
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
|
||||
{
|
||||
var value = reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
JsonTokenType.Number => reader.GetDouble().ToString(CultureInfo.InvariantCulture),
|
||||
JsonTokenType.True => bool.TrueString,
|
||||
JsonTokenType.False => bool.FalseString,
|
||||
JsonTokenType.Null => null,
|
||||
_ => throw new JsonException($"Unsupported token '{reader.TokenType}' in multi value field.")
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
values.Add(value.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
return values.Count == 0 ? null : string.Join(PlatformConsts.MultiValueDelimiter, values);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value);
|
||||
}
|
||||
}
|
||||
|
|
@ -79,7 +79,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
PermissionJson = DefaultPermissionJson(AbpIdentity.PermissionGroups.Create, AbpIdentity.PermissionGroups.Default, AbpIdentity.PermissionGroups.Update, AbpIdentity.PermissionGroups.Delete, AbpIdentity.PermissionGroups.Export, AbpIdentity.PermissionGroups.Import, AbpIdentity.PermissionGroups.Note),
|
||||
PagerOptionJson = DefaultPagerOptionJson,
|
||||
InsertFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
|
||||
new() { FieldName = "Id", FieldDbType = DbType.Guid, Value = "@ID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }
|
||||
new() { FieldName = "Id", FieldDbType = DbType.Guid, Value = "@NEWID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }
|
||||
}),
|
||||
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 350, true, true, true, true, false),
|
||||
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>()
|
||||
|
|
@ -215,7 +215,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
PagerOptionJson = DefaultPagerOptionJson,
|
||||
TreeOptionJson = DefaultTreeOptionJson("Name", "ParentName", true),
|
||||
InsertFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
|
||||
new() { FieldName = "Id", FieldDbType = DbType.Guid, Value = "@ID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }
|
||||
new() { FieldName = "Id", FieldDbType = DbType.Guid, Value = "@NEWID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }
|
||||
}),
|
||||
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 600, true, true, true, true, false),
|
||||
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>()
|
||||
|
|
|
|||
|
|
@ -1155,7 +1155,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
|||
PermissionJson = DefaultPermissionJson(listFormName),
|
||||
PagerOptionJson = DefaultPagerOptionJson,
|
||||
InsertFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
|
||||
new() { FieldName = "Id", FieldDbType = DbType.Guid, Value = "@ID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }
|
||||
new() { FieldName = "Id", FieldDbType = DbType.Guid, Value = "@NEWID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }
|
||||
}),
|
||||
DeleteCommand = $"DELETE FROM \"{TableNameResolver.GetFullTableName(nameof(TableNameEnum.GlobalSearch))}\" WHERE \"Id\"=@Id",
|
||||
DeleteFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
|
||||
|
|
|
|||
|
|
@ -165,6 +165,11 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
|
|||
- **Wizard dosyası** → tek başına taşınmaz; export zip'i `wizard/`, `custom/`, `crud/` ve
|
||||
`{sql|postgres}/{object|execute}/` bağımlılıklarını da içerir.
|
||||
- **`MenusData.json`** → `Routes` bölümüne dokunma; yalnızca `Modules` ve `Menus`.
|
||||
- **Çok değerli alan (`dxTagBox` / çoklu `dxGridBox`)** → tek metin kolonunda `|` ile saklanır.
|
||||
Ekran varsayılan `list-form-data/*` ucundan kaydediyorsa birleştirmeyi `QueryHelper` yapar;
|
||||
tipli DTO alan bir uca (`list-form-dynamic-api/...`, Custom Endpoint, Dynamic Service)
|
||||
bağlıysa DTO alanına `[JsonConverter(typeof(MultiValueStringJsonConverter))]` eklenir —
|
||||
yoksa istek `400` ile düşer. Ayrıntı: `lowcode.instructions.md` §4.3.2.1.
|
||||
|
||||
### Definition of done
|
||||
|
||||
|
|
|
|||
|
|
@ -83,11 +83,30 @@ const useListFormCustomDataSource = ({
|
|||
layout?: ListViewLayoutType | string,
|
||||
cols?: GridColumnData[] | CardViewTypes.Column[],
|
||||
) => {
|
||||
// Tree'de satir anahtari hiyerarsiyi kuran treeOptionDto.keyExpr alanidir (orn. "Name"),
|
||||
// fakat insert/update/delete uclari her zaman ListForm'daki KeyFieldName ile eslesme yapar.
|
||||
// Iki alan farkliysa sunucuya satirin gercek anahtar degeri gonderilir; aksi halde
|
||||
// WHERE sartina hicbir kayit dusmez ve islem sessizce bos doner.
|
||||
const storeKeyFieldName =
|
||||
layout === layoutTypes.tree
|
||||
? gridOptions.treeOptionDto?.keyExpr
|
||||
: gridOptions.keyFieldName
|
||||
const usesAlternateStoreKey =
|
||||
!!gridOptions.keyFieldName &&
|
||||
!!storeKeyFieldName &&
|
||||
storeKeyFieldName !== gridOptions.keyFieldName
|
||||
const primaryKeyByStoreKey = new Map<unknown, unknown>()
|
||||
const toPrimaryKey = (key: unknown) => {
|
||||
if (!usesAlternateStoreKey) return key
|
||||
|
||||
const rowData = (gridRef?.current?.instance() as any)?.getNodeByKey?.(key)?.data
|
||||
const primaryKey = rowData?.[gridOptions.keyFieldName as string]
|
||||
|
||||
return primaryKey ?? primaryKeyByStoreKey.get(key) ?? key
|
||||
}
|
||||
|
||||
const store: any = new CustomStore({
|
||||
key:
|
||||
layout === layoutTypes.tree
|
||||
? gridOptions.treeOptionDto?.keyExpr
|
||||
: gridOptions.keyFieldName,
|
||||
key: storeKeyFieldName,
|
||||
useDefaultSearch: true,
|
||||
load: async (loadOptions) => {
|
||||
// URL'den sort parametresini al ve loadOptions'a ekle
|
||||
|
|
@ -292,6 +311,16 @@ const useListFormCustomDataSource = ({
|
|||
summary: response.data.summary,
|
||||
groupCount: response.data.groupCount,
|
||||
}
|
||||
|
||||
if (usesAlternateStoreKey) {
|
||||
retValue.data.forEach((row: any) => {
|
||||
const storeKey = row?.[storeKeyFieldName as string]
|
||||
const primaryKey = row?.[gridOptions.keyFieldName as string]
|
||||
if (storeKey !== undefined && storeKey !== null && primaryKey !== undefined) {
|
||||
primaryKeyByStoreKey.set(storeKey, primaryKey)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return retValue
|
||||
} catch (error) {
|
||||
|
|
@ -373,7 +402,7 @@ const useListFormCustomDataSource = ({
|
|||
const deleteUrl = getServiceAddress(gridOptions.deleteServiceAddress)
|
||||
|
||||
return dynamicFetch(deleteUrl, 'POST', searchParams, {
|
||||
keys: [key],
|
||||
keys: [toPrimaryKey(key)],
|
||||
listFormCode,
|
||||
})
|
||||
},
|
||||
|
|
@ -384,7 +413,7 @@ const useListFormCustomDataSource = ({
|
|||
const updateUrl = getServiceAddress(gridOptions.updateServiceAddress)
|
||||
|
||||
return dynamicFetch(updateUrl, 'POST', searchParams, {
|
||||
keys: [key],
|
||||
keys: [toPrimaryKey(key)],
|
||||
data: values,
|
||||
listFormCode,
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue