Wizard Permission Group Name düzenlemesi

This commit is contained in:
Sedat ÖZTÜRK 2026-08-26 09:40:18 +03:00
parent ec6b35d6d5
commit 9de6b1d05c
12 changed files with 571 additions and 59 deletions

View file

@ -11,6 +11,9 @@ public interface IListFormWizardAppService
Task<WizardSeedFileDto> GetFile(string fileName); Task<WizardSeedFileDto> GetFile(string fileName);
Task DeleteFile(string fileName); Task DeleteFile(string fileName);
/// <summary>Izin gruplarini iki dilli gorunen adlariyla dondurur.</summary>
Task<List<WizardPermissionGroupDto>> GetPermissionGroups();
/// <summary>Wizard dosyasini ve bagli seed dosyalarini zip olarak dondurur.</summary> /// <summary>Wizard dosyasini ve bagli seed dosyalarini zip olarak dondurur.</summary>
Task<IRemoteStreamContent> ExportFile(string fileName); Task<IRemoteStreamContent> ExportFile(string fileName);

View file

@ -75,6 +75,17 @@ public class ListFormWizardDto
public string LanguageTextMenuParentTr { get; set; } public string LanguageTextMenuParentTr { get; set; }
public string PermissionGroupName { get; set; } public string PermissionGroupName { get; set; }
/// <summary>
/// Izin grubunun Ingilizce gorunen adi. Grup yeni tanimlaniyorsa grup adiyla ayni olan dil
/// anahtarina bu metin yazilir; mevcut bir grup secildiginde bos gelirse sunucu veritabanindaki
/// degeri geri yazar, boylece seed dosyasi her durumda iki dilli gorunen adi tasir.
/// </summary>
public string PermissionGroupDisplayNameEn { get; set; }
/// <summary>Izin grubunun Turkce gorunen adi; <see cref="PermissionGroupDisplayNameEn"/> ile ayni kurallara tabidir.</summary>
public string PermissionGroupDisplayNameTr { get; set; }
public string MenuParentCode { get; set; } public string MenuParentCode { get; set; }
public string MenuParentShortName { get; set; } public string MenuParentShortName { get; set; }
public string MenuParentIcon { get; set; } public string MenuParentIcon { get; set; }

View file

@ -0,0 +1,12 @@
namespace Sozsoft.Platform.ListForms;
/// <summary>
/// Wizard izin grubu secim listesi ogesi. Gorunen adlar, grup adiyla ayni olan dil
/// anahtarinin metinlerinden okunur; secim yapildiginda forma bu degerler doldurulur.
/// </summary>
public class WizardPermissionGroupDto
{
public string Name { get; set; }
public string DisplayNameEn { get; set; }
public string DisplayNameTr { get; set; }
}

View file

@ -138,15 +138,7 @@ public class ListFormWizardAppService(
? PlatformConsts.AppName ? PlatformConsts.AppName
: input.PermissionGroupName.Trim(); : input.PermissionGroupName.Trim();
input.PermissionGroupName = groupName; input.PermissionGroupName = groupName;
if (!await repoPermGroup.AnyAsync(a => a.Name == groupName)) await ApplyPermissionGroupAsync(input, groupName, inserted);
{
await repoPermGroup.InsertAsync(new PermissionGroupDefinitionRecord(GuidGenerator.Create(), groupName, groupName), autoSave: false);
if (string.Equals(groupName, input.MenuParentCode, StringComparison.OrdinalIgnoreCase))
await EnsureLangKey(groupName, inserted);
else
await CreateLangKey(groupName, groupName, groupName, inserted);
inserted.PermissionGroupNames.Add(groupName);
}
// Permission'ları tek seferde kontrol et ve oluştur // Permission'ları tek seferde kontrol et ve oluştur
var existingPerms = await repoPerm.GetListAsync(a => a.GroupName == groupName); var existingPerms = await repoPerm.GetListAsync(a => a.GroupName == groupName);
@ -676,6 +668,33 @@ public class ListFormWizardAppService(
return routePath; return routePath;
} }
/// <summary>
/// Izin gruplarini iki dilli gorunen adlariyla dondurur. Gorunen ad, grup adiyla ayni olan
/// dil anahtarinin metnidir; wizard formu secim yapildiginda bu degerleri doldurur.
/// </summary>
public async Task<List<WizardPermissionGroupDto>> GetPermissionGroups()
{
var res = PlatformConsts.AppName;
var groups = await repoPermGroup.GetListAsync();
var names = groups.Where(g => g.Name != null).Select(g => g.Name!).ToList();
var texts = await repoLangText.GetListAsync(a => a.ResourceName == res && names.Contains(a.Key));
var textsByKey = texts.Where(t => t.Key != null).GroupBy(t => t.Key!).ToDictionary(g => g.Key, g => g.ToList());
return [.. groups
.OrderBy(g => g.Name)
.Select(g =>
{
textsByKey.TryGetValue(g.Name, out var groupTexts);
return new WizardPermissionGroupDto
{
Name = g.Name,
DisplayNameEn = groupTexts?.FirstOrDefault(t => t.CultureName == cultureNameDefault)?.Value ?? g.DisplayName,
DisplayNameTr = groupTexts?.FirstOrDefault(t => t.CultureName == LanguageCodes.Tr)?.Value ?? g.DisplayName
};
})];
}
public async Task<List<WizardFileInfoDto>> GetFiles() public async Task<List<WizardFileInfoDto>> GetFiles()
{ {
var outputPath = ResolveWizardSeedOutputPath(); var outputPath = ResolveWizardSeedOutputPath();
@ -1575,6 +1594,70 @@ public class ListFormWizardAppService(
} }
} }
/// <summary>
/// Izin grubunu ve iki dilli gorunen adini hazirlar.
/// <para>
/// Grup yeniyse kaydi ve grup adiyla ayni olan dil anahtarini uretir; gorunen ad
/// girilmediyse grup adina duser. Grup zaten varsa veritabani metinlerine dokunulmaz,
/// yalnizca kullanici acikca farkli bir metin girdiyse guncellenir. Her durumda
/// <paramref name="input"/> uzerindeki gorunen adlar doldurulur; seed dosyasi mevcut bir
/// grup secildiginde de iki dilli gorunen adi tasimalidir.
/// </para>
/// </summary>
private async Task ApplyPermissionGroupAsync(ListFormWizardDto input, string groupName, WizardInsertedRecordsDto inserted)
{
var displayEn = input.PermissionGroupDisplayNameEn?.Trim();
var displayTr = input.PermissionGroupDisplayNameTr?.Trim();
if (!await repoPermGroup.AnyAsync(a => a.Name == groupName))
{
await repoPermGroup.InsertAsync(new PermissionGroupDefinitionRecord(GuidGenerator.Create(), groupName, groupName), autoSave: false);
// Grup adi ust menu koduyla ayniysa dil anahtari menu metinleriyle zaten uretilir;
// burada uzerine yazmak menu adini bozar.
if (string.Equals(groupName, input.MenuParentCode, StringComparison.OrdinalIgnoreCase))
{
await EnsureLangKey(groupName, inserted);
displayEn = string.IsNullOrWhiteSpace(displayEn) ? input.LanguageTextMenuParentEn?.Trim() : displayEn;
displayTr = string.IsNullOrWhiteSpace(displayTr) ? input.LanguageTextMenuParentTr?.Trim() : displayTr;
}
else
{
displayEn = string.IsNullOrWhiteSpace(displayEn) ? groupName : displayEn;
displayTr = string.IsNullOrWhiteSpace(displayTr) ? groupName : displayTr;
await CreateLangKey(groupName, displayEn, displayTr, inserted);
}
inserted.PermissionGroupNames.Add(groupName);
}
else
{
var (existingEn, existingTr) = await GetLangTextsAsync(groupName);
// Mevcut grubun metinleri yalnizca kullanici bilerek degistirdiginde guncellenir.
var changed = (!string.IsNullOrWhiteSpace(displayEn) && displayEn != existingEn)
|| (!string.IsNullOrWhiteSpace(displayTr) && displayTr != existingTr);
displayEn = string.IsNullOrWhiteSpace(displayEn) ? (existingEn ?? groupName) : displayEn;
displayTr = string.IsNullOrWhiteSpace(displayTr) ? (existingTr ?? groupName) : displayTr;
if (changed)
await CreateLangKey(groupName, displayEn, displayTr, inserted);
}
input.PermissionGroupDisplayNameEn = string.IsNullOrWhiteSpace(displayEn) ? groupName : displayEn;
input.PermissionGroupDisplayNameTr = string.IsNullOrWhiteSpace(displayTr) ? groupName : displayTr;
}
/// <summary>Bir dil anahtarinin Ingilizce/Turkce metinlerini dondurur; yoksa null.</summary>
private async Task<(string? En, string? Tr)> GetLangTextsAsync(string key)
{
var res = PlatformConsts.AppName;
var texts = await repoLangText.GetListAsync(a => a.ResourceName == res && a.Key == key);
return (texts.FirstOrDefault(a => a.CultureName == cultureNameDefault)?.Value,
texts.FirstOrDefault(a => a.CultureName == LanguageCodes.Tr)?.Value);
}
private async Task<LanguageKey> CreateLangKey(string key, string textEn, string textTr, WizardInsertedRecordsDto inserted = null) private async Task<LanguageKey> CreateLangKey(string key, string textEn, string textTr, WizardInsertedRecordsDto inserted = null)
{ {
var res = PlatformConsts.AppName; var res = PlatformConsts.AppName;

View file

@ -16290,6 +16290,48 @@
"en": "Permission Group Name", "en": "Permission Group Name",
"tr": "İzin Grubu Adı" "tr": "İzin Grubu Adı"
}, },
{
"resourceName": "Platform",
"key": "App.WizardStep1.PermissionGroupDefine",
"en": "Define Permission Group",
"tr": "İzin Grubu Tanımla"
},
{
"resourceName": "Platform",
"key": "App.WizardStep1.PermissionGroupDisplayName",
"en": "Permission Group Display Name",
"tr": "İzin Grubu Görünen Adı"
},
{
"resourceName": "Platform",
"key": "App.WizardStep1.PermissionGroupDisplayNameRequired",
"en": "Define the display names of the permission group",
"tr": "İzin grubunun görünen adlarını tanımlayın"
},
{
"resourceName": "Platform",
"key": "App.WizardStep1.PermissionGroupNewHint",
"en": "The group will be created on deploy with these display names.",
"tr": "Grup, deploy sırasında bu görünen adlarla oluşturulur."
},
{
"resourceName": "Platform",
"key": "App.WizardStep1.PermissionGroupExistsHint",
"en": "This group already exists; changing the display names updates its texts on deploy.",
"tr": "Bu grup zaten var; görünen adları değiştirmek deploy sırasında metinlerini günceller."
},
{
"resourceName": "Platform",
"key": "App.WizardStep1.PermissionGroupDisplayNameEn",
"en": "Permission Group Display Name (English)",
"tr": "İzin Grubu Görünen Adı (İngilizce)"
},
{
"resourceName": "Platform",
"key": "App.WizardStep1.PermissionGroupDisplayNameTr",
"en": "Permission Group Display Name (Turkish)",
"tr": "İzin Grubu Görünen Adı (Türkçe)"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.WizardStep1.NoMenusAvailable", "key": "App.WizardStep1.NoMenusAvailable",

View file

@ -192,12 +192,15 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
{ {
await _repoPermGroup.InsertAsync( await _repoPermGroup.InsertAsync(
new PermissionGroupDefinitionRecord(Guid.NewGuid(), groupName, groupName), autoSave: true); new PermissionGroupDefinitionRecord(Guid.NewGuid(), groupName, groupName), autoSave: true);
if (string.Equals(groupName, input.MenuParentCode, StringComparison.OrdinalIgnoreCase))
await EnsureLangKeyAsync(groupName);
else
await CreateLangKeyAsync(groupName, groupName, groupName);
} }
// Grup adi ust menu koduyla ayniysa dil anahtari menu metinleriyle uretilir; aksi halde
// gorunen adlar yazilir. CreateLangKeyAsync mevcut metinlerin uzerine yazmaz.
if (string.Equals(groupName, input.MenuParentCode, StringComparison.OrdinalIgnoreCase))
await EnsureLangKeyAsync(groupName);
else
await CreateLangKeyAsync(groupName, input.PermissionGroupDisplayNameEn?.Trim(), input.PermissionGroupDisplayNameTr?.Trim());
// Permissions - tek seferde mevcut permission'ları çek, sonra her birini kontrol et // Permissions - tek seferde mevcut permission'ları çek, sonra her birini kontrol et
var permQueryable = await _repoPerm.GetQueryableAsync(); var permQueryable = await _repoPerm.GetQueryableAsync();
var existingPerms = permQueryable.Where(a => a.GroupName == groupName).ToList(); var existingPerms = permQueryable.Where(a => a.GroupName == groupName).ToList();

View file

@ -33,6 +33,8 @@
"LanguageTextMenuParentEn": "Sample", "LanguageTextMenuParentEn": "Sample",
"LanguageTextMenuParentTr": "Sample", "LanguageTextMenuParentTr": "Sample",
"PermissionGroupName": "App.Wizard.Sample", "PermissionGroupName": "App.Wizard.Sample",
"PermissionGroupDisplayNameEn": "Sample",
"PermissionGroupDisplayNameTr": "Sample",
"MenuParentCode": "App.Wizard.Sample", "MenuParentCode": "App.Wizard.Sample",
"MenuParentShortName": "Sam", "MenuParentShortName": "Sam",
"MenuParentIcon": "FcBookmark", "MenuParentIcon": "FcBookmark",

View file

@ -84,6 +84,10 @@ export interface ListFormWizardDto {
languageTextMenuParentEn: string languageTextMenuParentEn: string
languageTextMenuParentTr: string languageTextMenuParentTr: string
permissionGroupName: string permissionGroupName: string
/** Izin grubunun Ingilizce gorunen adi; grup adiyla ayni olan dil anahtarina yazilir. */
permissionGroupDisplayNameEn: string
/** Izin grubunun Turkce gorunen adi. */
permissionGroupDisplayNameTr: string
menuParentCode: string menuParentCode: string
menuParentShortName?: string menuParentShortName?: string
menuParentIcon?: string menuParentIcon?: string
@ -106,6 +110,13 @@ export interface ListFormWizardDto {
widgets?: WidgetEditDto[] widgets?: WidgetEditDto[]
} }
/** Izin grubu secim listesi ogesi; gorunen adlar grup adiyla ayni olan dil anahtarindan gelir. */
export interface WizardPermissionGroupDto {
name: string
displayNameEn: string
displayNameTr: string
}
export interface WizardFileInfoDto { export interface WizardFileInfoDto {
fileName: string fileName: string
wizardName: string wizardName: string

View file

@ -3,6 +3,7 @@ import {
WizardFileInfoDto, WizardFileInfoDto,
WizardImportAnalysisDto, WizardImportAnalysisDto,
WizardImportApplyResultDto, WizardImportApplyResultDto,
WizardPermissionGroupDto,
WizardSeedFileDto, WizardSeedFileDto,
} from '@/proxy/admin/wizard/models' } from '@/proxy/admin/wizard/models'
import apiService from './api.service' import apiService from './api.service'
@ -14,6 +15,13 @@ export const postListFormWizard = (input: ListFormWizardDto) =>
data: input, data: input,
}) })
/** Izin gruplarini iki dilli gorunen adlariyla getirir. */
export const getWizardPermissionGroups = () =>
apiService.fetchData<WizardPermissionGroupDto[]>({
method: 'GET',
url: `/api/app/list-form-wizard/permission-groups`,
})
export const getWizardFiles = () => export const getWizardFiles = () =>
apiService.fetchData<WizardFileInfoDto[]>({ apiService.fetchData<WizardFileInfoDto[]>({
method: 'GET', method: 'GET',

View file

@ -10,7 +10,6 @@ import { useLocation, useNavigate } from 'react-router-dom'
import * as Yup from 'yup' import * as Yup from 'yup'
import { FaFolderOpen } from 'react-icons/fa' import { FaFolderOpen } from 'react-icons/fa'
import { getMenus } from '@/services/menu.service' import { getMenus } from '@/services/menu.service'
import { getPermissions } from '@/services/identity.service'
import { getDataSources } from '@/services/data-source.service' import { getDataSources } from '@/services/data-source.service'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service' import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import type { SqlObjectExplorerDto, DatabaseColumnDto } from '@/proxy/sql-query-manager/models' import type { SqlObjectExplorerDto, DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
@ -35,7 +34,13 @@ import WizardStepDeploy from './WizardStepDeploy'
import { Container } from '@/components/shared' import { Container } from '@/components/shared'
import { sqlDataTypeToDbType } from '../edit/options' import { sqlDataTypeToDbType } from '../edit/options'
import { useStoreActions } from '@/store/store' import { useStoreActions } from '@/store/store'
import { getWizardFile, getWizardFiles, postListFormWizard } from '@/services/wizard.service' import { useStoreState } from '@/store'
import {
getWizardFile,
getWizardFiles,
getWizardPermissionGroups,
postListFormWizard,
} from '@/services/wizard.service'
import { ListFormWizardDto, WizardComponentKindEnum } from '@/proxy/admin/wizard/models' import { ListFormWizardDto, WizardComponentKindEnum } from '@/proxy/admin/wizard/models'
import WizardStepComponent from './WizardStepComponent' import WizardStepComponent from './WizardStepComponent'
import WizardStepCustomComponent from './WizardStepCustomComponent' import WizardStepCustomComponent from './WizardStepCustomComponent'
@ -158,6 +163,8 @@ const initialValues: ListFormWizardDto = {
languageTextMenuParentEn: '', languageTextMenuParentEn: '',
languageTextMenuParentTr: '', languageTextMenuParentTr: '',
permissionGroupName: '', permissionGroupName: '',
permissionGroupDisplayNameEn: '',
permissionGroupDisplayNameTr: '',
menuParentCode: '', menuParentCode: '',
menuParentShortName: '', menuParentShortName: '',
menuParentIcon: '', menuParentIcon: '',
@ -186,6 +193,9 @@ const step1ValidationSchema = Yup.object().shape({
createMenu: Yup.boolean(), createMenu: Yup.boolean(),
// İzin grubu menüden bağımsızdır: menüsüz wizard'da da izinler üretilir ve bir gruba yazılır. // İzin grubu menüden bağımsızdır: menüsüz wizard'da da izinler üretilir ve bir gruba yazılır.
permissionGroupName: Yup.string().required(), permissionGroupName: Yup.string().required(),
// Grup adi ile birlikte iki dilli gorunen ad da seed dosyasina yazilir; bos birakilamaz.
permissionGroupDisplayNameEn: Yup.string().required(),
permissionGroupDisplayNameTr: Yup.string().required(),
languageTextMenuEn: Yup.string().required(), languageTextMenuEn: Yup.string().required(),
languageTextMenuTr: Yup.string().required(), languageTextMenuTr: Yup.string().required(),
}) })
@ -542,7 +552,17 @@ const Wizard = () => {
const [isLoadingMenu, setIsLoadingMenu] = useState(false) const [isLoadingMenu, setIsLoadingMenu] = useState(false)
const [menuTree, setMenuTree] = useState<MenuTreeNode[]>([]) const [menuTree, setMenuTree] = useState<MenuTreeNode[]>([])
const [rawMenuItems, setRawMenuItems] = useState<(MenuItem & { id?: string })[]>([]) const [rawMenuItems, setRawMenuItems] = useState<(MenuItem & { id?: string })[]>([])
const { translate } = useLocalization() const { translate, cultureName } = useLocalization()
// Dil secimi store'da tutulur; abpConfig'teki kultur, secim sonrasi config yenilenene
// kadar eski degeri tasiyabildigi icin once store okunur.
const currentLang = useStoreState((state) => state.locale.currentLang)
/** Izin grubu etiketi, uygulamanin acik olan dilinde gosterilir. */
const isTurkishUi = (currentLang || cultureName || '').toLowerCase().startsWith('tr')
const permissionGroupLabel = (group: {
name: string
displayNameEn: string
displayNameTr: string
}) => (isTurkishUi ? group.displayNameTr : group.displayNameEn) || group.name
const getMenuList = async () => { const getMenuList = async () => {
setIsLoadingMenu(true) setIsLoadingMenu(true)
const response = await getMenus() const response = await getMenus()
@ -558,17 +578,83 @@ const Wizard = () => {
// ── Permission Groups ── // ── Permission Groups ──
const [isLoadingPermissionGroup, setIsLoadingPermissionGroup] = useState(false) const [isLoadingPermissionGroup, setIsLoadingPermissionGroup] = useState(false)
const [permissionGroupList, setPermissionGroupList] = useState<SelectBoxOption[]>([]) const [permissionGroupList, setPermissionGroupList] = useState<SelectBoxOption[]>([])
// Secilen grubun iki dilli gorunen adini forma doldurmak icin ad -> metin esleme.
const permissionGroupTextsRef = useRef<Record<string, { en: string; tr: string }>>({})
const getPermissionGroupList = async () => { const getPermissionGroupList = async () => {
setIsLoadingPermissionGroup(true) setIsLoadingPermissionGroup(true)
const response = await getPermissions('R', '') const response = await getWizardPermissionGroups()
if (response.data?.groups) { const groups = response.data ?? []
setPermissionGroupList( permissionGroupTextsRef.current = groups.reduce(
response.data.groups.map((item: any) => ({ value: item.name, label: item.displayName })), (acc, item) => ({
) ...acc,
} [item.name]: { en: item.displayNameEn ?? '', tr: item.displayNameTr ?? '' },
}),
{} as Record<string, { en: string; tr: string }>,
)
setPermissionGroupList(
groups.map((item) => ({ value: item.name, label: permissionGroupLabel(item) })),
)
setIsLoadingPermissionGroup(false) setIsLoadingPermissionGroup(false)
} }
/**
* Izin grubu secimi/olusturulmasi. Mevcut bir grup secildiginde gorunen adlar veritabanindaki
* degerlerle doldurulur; yeni bir ad yazildiginda grup adi ilk deger olarak onerilir. Her iki
* durumda da alanlar dolu kalir, cunku seed dosyasi iki dilli gorunen adi tasimak zorundadir.
*/
const handlePermissionGroupChange = (
groupName: string,
/** Grup mevcut degilse kullanilacak metinler; yeni olusturulan menu henuz listede yoktur. */
fallbackTexts?: { en: string; tr: string },
) => {
const formik = formikRef.current
formik?.setFieldValue('permissionGroupName', groupName)
if (!groupName) {
formik?.setFieldValue('permissionGroupDisplayNameEn', '')
formik?.setFieldValue('permissionGroupDisplayNameTr', '')
return
}
// Sirasiyla: mevcut grubun metinleri, cagiranin verdigi metinler, ayni koda sahip menunun
// metinleri. Grup adi kod oldugu icin en son care olarak kullanilir.
const texts = permissionGroupTextsRef.current[groupName]
const menu = rawMenuItems.find((item) => item.code === groupName)
formik?.setFieldValue(
'permissionGroupDisplayNameEn',
texts?.en || fallbackTexts?.en || menu?.menuTextEn || groupName,
)
formik?.setFieldValue(
'permissionGroupDisplayNameTr',
texts?.tr || fallbackTexts?.tr || menu?.menuTextTr || groupName,
)
}
/**
* Dialogda tanimlanan grup. Kayit veritabanina yazilmaz; deploy adiminda grup yoksa uretilir,
* varsa metinler yalnizca burada degistirildiginde guncellenir. Yeni ad, secim listesine de
* eklenir ki kullanici adimlar arasinda gidip gelince secim kaybolmasin.
*/
const handlePermissionGroupDefine = (group: {
name: string
displayNameEn: string
displayNameTr: string
}) => {
const formik = formikRef.current
permissionGroupTextsRef.current[group.name] = {
en: group.displayNameEn,
tr: group.displayNameTr,
}
setPermissionGroupList((prev) =>
prev.some((o) => o.value === group.name)
? prev.map((o) =>
o.value === group.name ? { ...o, label: permissionGroupLabel(group) } : o,
)
: [...prev, { value: group.name, label: permissionGroupLabel(group) }],
)
formik?.setFieldValue('permissionGroupName', group.name)
formik?.setFieldValue('permissionGroupDisplayNameEn', group.displayNameEn)
formik?.setFieldValue('permissionGroupDisplayNameTr', group.displayNameTr)
}
useEffect(() => { useEffect(() => {
getDataSourceList() getDataSourceList()
getMenuList() getMenuList()
@ -646,6 +732,8 @@ const Wizard = () => {
languageTextMenuParentEn: w.languageTextMenuParentEn ?? '', languageTextMenuParentEn: w.languageTextMenuParentEn ?? '',
languageTextMenuParentTr: w.languageTextMenuParentTr ?? '', languageTextMenuParentTr: w.languageTextMenuParentTr ?? '',
permissionGroupName: w.permissionGroupName ?? '', permissionGroupName: w.permissionGroupName ?? '',
permissionGroupDisplayNameEn: w.permissionGroupDisplayNameEn ?? '',
permissionGroupDisplayNameTr: w.permissionGroupDisplayNameTr ?? '',
menuParentCode: w.menuParentCode ?? '', menuParentCode: w.menuParentCode ?? '',
menuParentShortName: w.menuParentShortName ?? '', menuParentShortName: w.menuParentShortName ?? '',
menuParentIcon: w.menuParentIcon ?? '', menuParentIcon: w.menuParentIcon ?? '',
@ -810,19 +898,23 @@ const Wizard = () => {
return null return null
} }
const applyPermissionGroupFromRoot = (rootCode: string) => { const applyPermissionGroupFromRoot = (
rootCode: string,
/** Kok menu heniz listede degilse (yeni eklendi) gorunen ad olarak kullanilacak metinler. */
fallbackTexts?: { en: string; tr: string },
) => {
const rootItem = rawMenuItems.find((i) => i.code === rootCode) const rootItem = rawMenuItems.find((i) => i.code === rootCode)
// 1. Use group field if set // 1. Use group field if set
if (rootItem?.group) { if (rootItem?.group) {
formikRef.current?.setFieldValue('permissionGroupName', rootItem.group) handlePermissionGroupChange(rootItem.group)
return return
} }
// 2. Match root code exactly against permission group names (e.g. "App.Saas" -> "App.Saas") // 2. Match root code exactly against permission group names (e.g. "App.Saas" -> "App.Saas")
const exactMatch = permissionGroupList.find((g) => g.value === rootCode) const exactMatch = permissionGroupList.find((g) => g.value === rootCode)
if (exactMatch) { if (exactMatch) {
formikRef.current?.setFieldValue('permissionGroupName', exactMatch.value) handlePermissionGroupChange(exactMatch.value ?? '')
return return
} }
@ -833,13 +925,53 @@ const Wizard = () => {
g.value?.toLowerCase().includes(lastSegment), g.value?.toLowerCase().includes(lastSegment),
) )
if (partialMatch) { if (partialMatch) {
formikRef.current?.setFieldValue('permissionGroupName', partialMatch.value) handlePermissionGroupChange(partialMatch.value ?? '')
return return
} }
} }
// 4. Fallback: set rootCode as a new creatable value (SelectBox supports free input) // 4. Fallback: set rootCode as a new creatable value (SelectBox supports free input)
formikRef.current?.setFieldValue('permissionGroupName', rootCode) handlePermissionGroupChange(rootCode, fallbackTexts)
}
/**
* Ust menu secimi kaldirilir. Izin grubu yalnizca kaldirilan menuden turetilmisse
* (grup adi menunun kendisi ya da kok kodu ile ayni) temizlenir; kullanici baska bir grup
* sectiyse secimi korunur.
*/
const handleClearMenuParent = () => {
const formik = formikRef.current
const parentCode = formik?.values.menuParentCode ?? ''
const group = (formik?.values.permissionGroupName ?? '').toLowerCase()
const derivedFromParent =
!!group &&
(group === parentCode.toLowerCase() ||
group === findRootCode(rawMenuItems, parentCode).toLowerCase())
formik?.setFieldValue('menuParentCode', '')
formik?.setFieldValue('menuParentShortName', '')
formik?.setFieldValue('menuParentIcon', '')
formik?.setFieldValue('languageTextMenuParentEn', '')
formik?.setFieldValue('languageTextMenuParentTr', '')
if (derivedFromParent) handlePermissionGroupChange('')
}
/**
* Menu agacindan bir menu silindiginde, o koda bagli secimler formda kalmamalidir:
* secili ust menu ve ayni koda sahip izin grubu temizlenir.
*/
const handleMenuDeleted = (code: string) => {
const formik = formikRef.current
const deleted = code.toLowerCase()
if ((formik?.values.permissionGroupName ?? '').toLowerCase() === deleted)
handlePermissionGroupChange('')
if ((formik?.values.menuParentCode ?? '').toLowerCase() === deleted) {
formik?.setFieldValue('menuParentCode', '')
formik?.setFieldValue('menuParentShortName', '')
formik?.setFieldValue('menuParentIcon', '')
formik?.setFieldValue('languageTextMenuParentEn', '')
formik?.setFieldValue('languageTextMenuParentTr', '')
}
} }
/** /**
@ -908,7 +1040,12 @@ const Wizard = () => {
'menuParentShortName', 'menuParentShortName',
menu.parentCode ? (existingRoot?.shortName ?? '') : (menu.shortName ?? ''), menu.parentCode ? (existingRoot?.shortName ?? '') : (menu.shortName ?? ''),
) )
applyPermissionGroupFromRoot(rootCode) // Yeni menu kok seviyedeyse izin grubu ondan turetilir; gorunen adlar da menunun
// dialogda girilen TR/EN metinleridir (menu heniz listede olmadigi icin buradan gecilir).
applyPermissionGroupFromRoot(
rootCode,
menu.parentCode ? undefined : { en: menu.menuTextEn, tr: menu.menuTextTr },
)
await getMenuList() await getMenuList()
} }
@ -1210,15 +1347,14 @@ const Wizard = () => {
isLoadingMenu={isLoadingMenu} isLoadingMenu={isLoadingMenu}
onMenuParentChange={handleMenuParentChange} onMenuParentChange={handleMenuParentChange}
onCreateMenuChange={handleCreateMenuChange} onCreateMenuChange={handleCreateMenuChange}
onClearMenuParent={() => { onClearMenuParent={handleClearMenuParent}
formikRef.current?.setFieldValue('menuParentCode', '') onMenuDeleted={handleMenuDeleted}
formikRef.current?.setFieldValue('menuParentShortName', '')
formikRef.current?.setFieldValue('menuParentIcon', '')
}}
onMenuCreated={handleMenuCreated} onMenuCreated={handleMenuCreated}
onReloadMenu={getMenuList} onReloadMenu={getMenuList}
permissionGroupList={permissionGroupList} permissionGroupList={permissionGroupList}
isLoadingPermissionGroup={isLoadingPermissionGroup} isLoadingPermissionGroup={isLoadingPermissionGroup}
onPermissionGroupChange={handlePermissionGroupChange}
onPermissionGroupDefine={handlePermissionGroupDefine}
onBack={() => onBack={() =>
setCurrentStep( setCurrentStep(
values.componentKind === WizardComponentKindEnum.Custom values.componentKind === WizardComponentKindEnum.Custom

View file

@ -1,4 +1,4 @@
import { Button } from '@/components/ui' import { Button } from '@/components/ui'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models' import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import React, { useMemo, useState } from 'react' import React, { useMemo, useState } from 'react'
import { import {
@ -92,6 +92,7 @@ function buildLogSteps(
label: translate('::App.Step4Log.ConfiguringPermission', { label: translate('::App.Step4Log.ConfiguringPermission', {
0: values.permissionGroupName, 0: values.permissionGroupName,
}), }),
detail: `EN: ${values.permissionGroupDisplayNameEn} / TR: ${values.permissionGroupDisplayNameTr}`,
}, },
{ {
id: 5, id: 5,
@ -122,6 +123,7 @@ function buildLogSteps(
label: translate('::App.Step4Log.ConfiguringPermission', { label: translate('::App.Step4Log.ConfiguringPermission', {
0: values.permissionGroupName, 0: values.permissionGroupName,
}), }),
detail: `EN: ${values.permissionGroupDisplayNameEn} / TR: ${values.permissionGroupDisplayNameTr}`,
}, },
{ {
id: 5, id: 5,
@ -320,6 +322,14 @@ const WizardStepDeploy = ({
label={translate('::App.WizardStep4.PermissionGroup')} label={translate('::App.WizardStep4.PermissionGroup')}
value={values.permissionGroupName} value={values.permissionGroupName}
/> />
<Row
label={translate('::App.WizardStep1.PermissionGroupDisplayNameTr')}
value={values.permissionGroupDisplayNameTr}
/>
<Row
label={translate('::App.WizardStep1.PermissionGroupDisplayNameEn')}
value={values.permissionGroupDisplayNameEn}
/>
<Row label={translate('::App.Listform.ListformField.Icon')} value={values.menuIcon} /> <Row label={translate('::App.Listform.ListformField.Icon')} value={values.menuIcon} />
<Row label={translate('::App.WizardStep4.MenuTr')} value={values.languageTextMenuTr} /> <Row label={translate('::App.WizardStep4.MenuTr')} value={values.languageTextMenuTr} />
<Row label={translate('::App.WizardStep4.MenuEn')} value={values.languageTextMenuEn} /> <Row label={translate('::App.WizardStep4.MenuEn')} value={values.languageTextMenuEn} />

View file

@ -1,4 +1,13 @@
import { Button, FormItem, Input, Notification, Select, Switcher, toast } from '@/components/ui' import {
Button,
Dialog,
FormItem,
Input,
Notification,
Select,
Switcher,
toast,
} from '@/components/ui'
import classNames from 'classnames' import classNames from 'classnames'
import { SelectBoxOption } from '@/types/shared' import { SelectBoxOption } from '@/types/shared'
import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config' import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config'
@ -246,6 +255,8 @@ interface MenuTreeInlineProps {
isLoading: boolean isLoading: boolean
invalid?: boolean invalid?: boolean
onReload: () => void onReload: () => void
/** Silinen menünün kodu; forma yazılmış bağlı seçimleri temizlemek için. */
onDeleted: (code: string) => void
/** Düzenleme dialogunu ilgili menü ile açar. */ /** Düzenleme dialogunu ilgili menü ile açar. */
onEditMenu: (node: MenuTreeNode & { id?: string }) => void onEditMenu: (node: MenuTreeNode & { id?: string }) => void
initialExpanded?: Set<string> initialExpanded?: Set<string>
@ -259,6 +270,7 @@ function MenuTreeInline({
isLoading, isLoading,
invalid, invalid,
onReload, onReload,
onDeleted,
onEditMenu, onEditMenu,
initialExpanded, initialExpanded,
}: MenuTreeInlineProps) { }: MenuTreeInlineProps) {
@ -295,6 +307,7 @@ function MenuTreeInline({
setSaving(true) setSaving(true)
try { try {
await menuService.delete(node.id) await menuService.delete(node.id)
onDeleted(node.code)
onReload() onReload()
} catch (e: any) { } catch (e: any) {
toast.push(<Notification title={e.message} type="danger" />, { toast.push(<Notification title={e.message} type="danger" />, {
@ -353,6 +366,146 @@ function MenuTreeInline({
) )
} }
// ─── PermissionGroupDialog ───────────────────────────────────────────────────
export interface PermissionGroupDefinition {
name: string
displayNameEn: string
displayNameTr: string
}
interface PermissionGroupDialogProps {
isOpen: boolean
onClose: () => void
/** Dialog acilirken forma yazilmis degerler; yeni tanimda bos gelir. */
initialValue: PermissionGroupDefinition
/** Mevcut izin gruplarinin adlari; girilen ad bunlardan biriyse uyari gosterilir. */
existingNames: string[]
onSave: (group: PermissionGroupDefinition) => void
}
/**
* Izin grubu tanimlama dialogu. AbpPermissionGroups'ta Name ve DisplayName ayni yazilir,
* gorunen ad dil anahtarindan cozulur; bu yuzden burada Name ile birlikte iki dilli gorunen
* ad alinir. Kayit yalnizca wizard formuna yazilir, veritabani kaydi deploy adiminda uretilir.
*/
function PermissionGroupDialog({
isOpen,
onClose,
initialValue,
existingNames,
onSave,
}: PermissionGroupDialogProps) {
const { translate } = useLocalization()
const [form, setForm] = useState<PermissionGroupDefinition>(initialValue)
useEffect(() => {
if (isOpen) setForm(initialValue)
}, [isOpen, initialValue])
const name = form.name.trim()
const isExisting = existingNames.some((n) => n.toLowerCase() === name.toLowerCase())
const canSave = !!name && !!form.displayNameEn.trim() && !!form.displayNameTr.trim()
const fieldCls =
'h-9 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-200 outline-none focus:border-indigo-400 w-full'
const labelCls = 'text-xs font-medium text-gray-500 dark:text-gray-400 mb-1'
return (
<Dialog isOpen={isOpen} width={620} onClose={onClose} onRequestClose={onClose}>
<div className="flex flex-col gap-5 p-1">
{/* Header */}
<div className="flex items-center gap-2 pb-1 border-b border-gray-100 dark:border-gray-700">
{isExisting ? (
<FaEdit className="text-indigo-500 text-sm" />
) : (
<FaPlus className="text-green-500 text-sm" />
)}
<h5 className="text-base font-semibold text-gray-800 dark:text-gray-100">
{translate('::App.WizardStep1.PermissionGroupDefine')}
</h5>
</div>
{/* Name */}
<div className="flex flex-col">
<label className={labelCls}>
{translate('::App.WizardStep1.PermissionGroupName')}{' '}
<span className="text-red-500">*</span>
</label>
<Input
unstyle
autoFocus
className={`${fieldCls} font-mono`}
placeholder="App.Wizard.MyGroup"
value={form.name}
onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))}
/>
</div>
{/* Display names */}
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col">
<label className={labelCls}>
{translate('::App.WizardStep1.PermissionGroupDisplayNameTr')}{' '}
<span className="text-red-500">*</span>
</label>
<Input
unstyle
className={fieldCls}
value={form.displayNameTr}
onChange={(e) => setForm((p) => ({ ...p, displayNameTr: e.target.value }))}
/>
</div>
<div className="flex flex-col">
<label className={labelCls}>
{translate('::App.WizardStep1.PermissionGroupDisplayNameEn')}{' '}
<span className="text-red-500">*</span>
</label>
<Input
unstyle
className={fieldCls}
value={form.displayNameEn}
onChange={(e) => setForm((p) => ({ ...p, displayNameEn: e.target.value }))}
/>
</div>
</div>
{/* Ad mevcut bir gruba denk geliyorsa deploy o grubun metinlerini gunceller. */}
<p className="text-xs text-gray-500 dark:text-gray-400">
{translate(
isExisting
? '::App.WizardStep1.PermissionGroupExistsHint'
: '::App.WizardStep1.PermissionGroupNewHint',
)}
</p>
{/* Footer */}
<div className="flex justify-end gap-2 pt-1 border-t border-gray-100 dark:border-gray-700">
<Button type="button" size="sm" variant="plain" onClick={onClose}>
{translate('::App.Platform.Cancel')}
</Button>
<Button
type="button"
size="sm"
variant="solid"
disabled={!canSave}
onClick={() => {
onSave({
name,
displayNameEn: form.displayNameEn.trim(),
displayNameTr: form.displayNameTr.trim(),
})
onClose()
}}
>
{translate('::App.Platform.Save')}
</Button>
</div>
</div>
</Dialog>
)
}
// ─── WizardStepMenu ────────────────────────────────────────────────────────────── // ─── WizardStepMenu ──────────────────────────────────────────────────────────────
export interface WizardStepMenuProps { export interface WizardStepMenuProps {
@ -369,7 +522,10 @@ export interface WizardStepMenuProps {
onMenuParentChange: (code: string) => void onMenuParentChange: (code: string) => void
/** Menuye eklenip eklenmeyecegi; false iken menuye ozel alanlar hic sorulmaz. */ /** Menuye eklenip eklenmeyecegi; false iken menuye ozel alanlar hic sorulmaz. */
onCreateMenuChange: (createMenu: boolean) => void onCreateMenuChange: (createMenu: boolean) => void
/** Ust menu secimini ve ona bagli olarak turetilen izin grubunu temizler. */
onClearMenuParent: () => void onClearMenuParent: () => void
/** Menu agacindan bir menu silindiginde cagrilir; koda bagli secimler temizlenir. */
onMenuDeleted: (code: string) => void
onMenuCreated: (menu: { onMenuCreated: (menu: {
code: string code: string
parentCode?: string parentCode?: string
@ -381,6 +537,10 @@ export interface WizardStepMenuProps {
onReloadMenu: () => void onReloadMenu: () => void
permissionGroupList: SelectBoxOption[] permissionGroupList: SelectBoxOption[]
isLoadingPermissionGroup: boolean isLoadingPermissionGroup: boolean
/** Listeden grup secildiginde iki dilli gorunen ad alanlarini da doldurur. */
onPermissionGroupChange: (groupName: string) => void
/** Dialogda tanimlanan grup adi ve iki dilli gorunen adi forma yazar. */
onPermissionGroupDefine: (group: PermissionGroupDefinition) => void
onBack: () => void onBack: () => void
onNext: () => void onNext: () => void
translate: (key: string) => string translate: (key: string) => string
@ -404,15 +564,19 @@ const WizardStepMenu = ({
onMenuParentChange, onMenuParentChange,
onCreateMenuChange, onCreateMenuChange,
onClearMenuParent, onClearMenuParent,
onMenuDeleted,
onMenuCreated, onMenuCreated,
onReloadMenu, onReloadMenu,
permissionGroupList, permissionGroupList,
isLoadingPermissionGroup, isLoadingPermissionGroup,
onPermissionGroupChange,
onPermissionGroupDefine,
onBack, onBack,
onNext, onNext,
translate, translate,
}: WizardStepMenuProps) => { }: WizardStepMenuProps) => {
const [menuDialogOpen, setMenuDialogOpen] = useState(false) const [menuDialogOpen, setMenuDialogOpen] = useState(false)
const [permissionGroupDialogOpen, setPermissionGroupDialogOpen] = useState(false)
const [menuDialogParentCode, setMenuDialogParentCode] = useState('') const [menuDialogParentCode, setMenuDialogParentCode] = useState('')
// Dolu ise dialog düzenleme modunda açılır. // Dolu ise dialog düzenleme modunda açılır.
const [menuBeingEdited, setMenuBeingEdited] = useState<(MenuItem & { id?: string }) | null>(null) const [menuBeingEdited, setMenuBeingEdited] = useState<(MenuItem & { id?: string }) | null>(null)
@ -463,6 +627,10 @@ const WizardStepMenu = ({
// burada kontrol edilmezse "Next" aktif görünüp sessizce çalışmıyordu. // burada kontrol edilmezse "Next" aktif görünüp sessizce çalışmıyordu.
createMenu && !(Number(values.menuOrder) >= 1) && translate('::App.WizardStep1.MenuOrder'), createMenu && !(Number(values.menuOrder) >= 1) && translate('::App.WizardStep1.MenuOrder'),
!values.permissionGroupName && translate('::App.WizardStep1.PermissionGroupName'), !values.permissionGroupName && translate('::App.WizardStep1.PermissionGroupName'),
!values.permissionGroupDisplayNameEn &&
translate('::App.WizardStep1.PermissionGroupDisplayNameEn'),
!values.permissionGroupDisplayNameTr &&
translate('::App.WizardStep1.PermissionGroupDisplayNameTr'),
!values.languageTextMenuEn && translate('::App.WizardStep4.MenuEn'), !values.languageTextMenuEn && translate('::App.WizardStep4.MenuEn'),
!values.languageTextMenuTr && translate('::App.WizardStep4.MenuTr'), !values.languageTextMenuTr && translate('::App.WizardStep4.MenuTr'),
createMenu && !values.menuIcon && translate('::App.WizardStep4.MenuIcon'), createMenu && !values.menuIcon && translate('::App.WizardStep4.MenuIcon'),
@ -590,6 +758,7 @@ const WizardStepMenu = ({
isLoading={isLoadingMenu} isLoading={isLoadingMenu}
invalid={false} invalid={false}
onReload={onReloadMenu} onReload={onReloadMenu}
onDeleted={onMenuDeleted}
onEditMenu={handleEditMenu} onEditMenu={handleEditMenu}
initialExpanded={ initialExpanded={
values.menuParentCode values.menuParentCode
@ -661,32 +830,54 @@ const WizardStepMenu = ({
errorMessage={errors.permissionGroupName} errorMessage={errors.permissionGroupName}
asterisk={true} asterisk={true}
> >
<Field type="text" autoComplete="off" name="permissionGroupName"> <div className="flex items-center gap-2">
{({ field, form }: FieldProps<string>) => ( <div className="flex-1">
<Select <Field type="text" autoComplete="off" name="permissionGroupName">
componentAs={CreatableSelect} {({ field, form }: FieldProps<string>) => (
field={field} <Select
form={form} field={field}
placeholder={translate('::App.WizardStep1.PermissionGroupName')} form={form}
isClearable={true} placeholder={translate('::App.WizardStep1.PermissionGroupName')}
isLoading={isLoadingPermissionGroup} isLoading={isLoadingPermissionGroup}
options={permissionGroupList} options={permissionGroupList}
value={ value={
values.permissionGroupName values.permissionGroupName
? (permissionGroupList?.find( ? (permissionGroupList?.find(
(o) => o.value === values.permissionGroupName, (o) => o.value === values.permissionGroupName,
) ?? { ) ?? {
label: values.permissionGroupName, label: values.permissionGroupName,
value: values.permissionGroupName, value: values.permissionGroupName,
}) })
: null : null
} }
onChange={(option) => form.setFieldValue(field.name, option?.value)} onChange={(option) => onPermissionGroupChange(option?.value ?? '')}
/> />
)} )}
</Field> </Field>
</div>
{/* Gorunen adlar dialogda tanimlanir; listeden secilen grupta da duzenlenebilir. */}
<Button
type="button"
size="sm"
icon={values.permissionGroupName ? <FaEdit /> : <FaPlus />}
title={translate('::App.WizardStep1.PermissionGroupDefine')}
onClick={() => setPermissionGroupDialogOpen(true)}
/>
</div>
</FormItem> </FormItem>
<PermissionGroupDialog
isOpen={permissionGroupDialogOpen}
existingNames={permissionGroupList.map((o) => o.value ?? '')}
initialValue={{
name: values.permissionGroupName ?? '',
displayNameEn: values.permissionGroupDisplayNameEn ?? '',
displayNameTr: values.permissionGroupDisplayNameTr ?? '',
}}
onClose={() => setPermissionGroupDialogOpen(false)}
onSave={onPermissionGroupDefine}
/>
{createMenu && ( {createMenu && (
<> <>
{/* Menu Icon */} {/* Menu Icon */}