Wizard Edit özelliği eklendi

This commit is contained in:
Sedat Öztürk 2026-05-03 12:52:22 +03:00
parent ea3e847490
commit b2dfb04879
16 changed files with 572 additions and 216 deletions

View file

@ -7,6 +7,7 @@ public interface IListFormWizardAppService
{
Task Create(ListFormWizardDto input);
Task<List<WizardFileInfoDto>> GetFiles();
Task<WizardSeedFileDto> GetFile(string fileName);
Task DeleteFile(string fileName);
}

View file

@ -398,6 +398,24 @@ public class ListFormWizardAppService(
return Task.FromResult(result);
}
public async Task<WizardSeedFileDto> GetFile(string fileName)
{
// Güvenlik: sadece dosya adı, path traversal yasak
if (fileName.Contains('/') || fileName.Contains('\\') || fileName.Contains(".."))
throw new Volo.Abp.AbpException("Geçersiz dosya adı.");
var outputPath = ResolveWizardSeedOutputPath();
var filePath = Path.Combine(outputPath, fileName);
if (!File.Exists(filePath))
throw new Volo.Abp.AbpException($"Dosya bulunamadı: {fileName}");
var json = await File.ReadAllTextAsync(filePath);
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var seed = JsonSerializer.Deserialize<WizardSeedFileDto>(json, options);
return seed ?? throw new Volo.Abp.AbpException("Dosya okunamadı.");
}
public async Task DeleteFile(string fileName)
{
// Güvenlik: sadece dosya adı, path traversal yasak

View file

@ -16040,6 +16040,12 @@
"en": "Menu Information",
"tr": "Menü Bilgileri"
},
{
"resourceName": "Platform",
"key": "ListForms.Wizard.AddNewRecord",
"en": "Add New Wizard",
"tr": "Yeni Sihirbaz Ekle"
},
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Required",

View file

@ -48,7 +48,7 @@
"EditorScript": "",
"ColSpan": 1,
"IsRequired": true,
"DbSourceType": 9,
"DbSourceType": 12,
"TurkishCaption": "Id",
"EnglishCaption": "Id",
"LookupDataSourceType": 1,
@ -64,7 +64,7 @@
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"DbSourceType": 9,
"DbSourceType": 12,
"TurkishCaption": "Branch Id",
"EnglishCaption": "Branch Id",
"LookupDataSourceType": 2,
@ -80,7 +80,7 @@
"EditorScript": "",
"ColSpan": 1,
"IsRequired": true,
"DbSourceType": 16,
"DbSourceType": 12,
"TurkishCaption": "Name",
"EnglishCaption": "Name",
"LookupDataSourceType": 1,
@ -96,7 +96,7 @@
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"DbSourceType": 9,
"DbSourceType": 12,
"TurkishCaption": "Parent Id",
"EnglishCaption": "Parent Id",
"LookupDataSourceType": 2,

View file

@ -1,4 +1,54 @@
{
"commit": "aac3f4a",
"releases": []
"commit": "ea3e847",
"releases": [
{
"version": "1.0.5",
"buildDate": "2026-04-28",
"commit": "b9cc68ff41d675fefadab79f5ea2c5e7f376708a",
"changeLog": [
"- AllowAdding özelliği eklendi.",
"- Kulllanıcı bazında Workhour özelliği eklendi.",
"- ConcurrentUser sınırlaması getirildi.",
"- Uygulamadan kullanıcıyı kickleme özelliği getirildi.",
"- Uygulama açıkken DbMigrator çağrısı yapıldı."
]
},
{
"version": "1.0.4",
"buildDate": "2026-03-30",
"commit": "e9ce256c0706de408e08a462bee9e4a72653f2c7",
"changeLog": [
"- Role Yetkilerini kopyalama",
"- Place Holder güncellemesi",
"- Kayıt kopyalama özelliği"
]
},
{
"version": "1.0.3",
"buildDate": "2026-03-28",
"commit": "130d35c37703935e34948419f2ee900cc1b6b28c",
"changeLog": [
"Report Template View And Design"
]
},
{
"version": "1.0.2",
"buildDate": "2026-03-21",
"commit": "196b0dc24b7a815586166679b58ab4f9334bd40e",
"changeLog": [
"Artificial Intelligence Chat Bot güncellemeleri"
]
},
{
"version": "1.0.1",
"buildDate": "2026-03-18",
"commit": "ac7784c030825158641e9042c50dede2067c0486",
"changeLog": [
"Public Site Designer",
"Menu Groups",
"Sql Query Manager",
"Listform Wizard"
]
}
]
}

View file

@ -4,7 +4,6 @@ import {
EditingFormDto,
ExtraFilterEditDto,
FieldsDefaultValueDto,
SelectCommandTypeEnum,
SubFormDto,
WidgetEditDto,
} from '../../form/models'
@ -15,71 +14,6 @@ import {
ChartValueAxisDto,
} from '../charts/models'
export interface ListFormWizardColumnItemDto {
dataField: string
editorType: string
editorOptions: string
editorScript: string
colSpan: number
isRequired: boolean
dbSourceType: number
}
export interface ListFormWizardColumnGroupDto {
caption: string
colCount: number
items: ListFormWizardColumnItemDto[]
}
export interface ListFormWizardDto {
wizardName: string
listFormCode: string
menuCode: string
isTenant: boolean
isBranch: boolean
isOrganizationUnit: boolean
allowAdding: boolean
allowUpdating: boolean
allowDeleting: boolean
confirmDelete: boolean
allowDetail: boolean
defaultLayout: ListViewLayoutType
grid: boolean
pivot: boolean
tree: boolean
chart: boolean
gantt: boolean
scheduler: boolean
languageTextMenuEn: string
languageTextMenuTr: string
languageTextTitleEn: string
languageTextTitleTr: string
languageTextDescEn: string
languageTextDescTr: string
languageTextMenuParentEn: string
languageTextMenuParentTr: string
permissionGroupName: string
menuParentCode: string
menuIcon: string
dataSourceCode: string
dataSourceConnectionString: string
selectCommandType: SelectCommandTypeEnum
selectCommand: string
keyFieldName: string
keyFieldDbSourceType: number
groups?: ListFormWizardColumnGroupDto[]
}
export interface WizardFileInfoDto {
fileName: string
wizardName: string
listFormCode: string
createdAt: string
hasInsertedRecords: boolean
}
export interface ListFormJsonRowDto {
id?: string
index: number

View file

@ -0,0 +1,109 @@
import { SelectCommandTypeEnum } from "@/proxy/form/models"
import { ListViewLayoutType } from "@/views/admin/listForm/edit/types"
export interface ListFormWizardColumnItemDto {
dataField: string
editorType: string
editorOptions: string
editorScript: string
colSpan: number
isRequired: boolean
dbSourceType: number
}
export interface ListFormWizardColumnGroupDto {
caption: string
colCount: number
items: ListFormWizardColumnItemDto[]
}
export interface ListFormWizardDto {
wizardName: string
listFormCode: string
menuCode: string
isTenant: boolean
isBranch: boolean
isOrganizationUnit: boolean
allowAdding: boolean
allowUpdating: boolean
allowDeleting: boolean
confirmDelete: boolean
allowDetail: boolean
defaultLayout: ListViewLayoutType
grid: boolean
pivot: boolean
tree: boolean
chart: boolean
gantt: boolean
scheduler: boolean
languageTextMenuEn: string
languageTextMenuTr: string
languageTextTitleEn: string
languageTextTitleTr: string
languageTextDescEn: string
languageTextDescTr: string
languageTextMenuParentEn: string
languageTextMenuParentTr: string
permissionGroupName: string
menuParentCode: string
menuIcon: string
dataSourceCode: string
dataSourceConnectionString: string
selectCommandType: SelectCommandTypeEnum
selectCommand: string
keyFieldName: string
keyFieldDbSourceType: number
groups?: ListFormWizardColumnGroupDto[]
}
export interface WizardFileInfoDto {
fileName: string
wizardName: string
listFormCode: string
createdAt: string
hasInsertedRecords: boolean
}
export interface WizardInsertedRecordsDto {
languageKeys: string[]
permissionGroupNames: string[]
permissionNames: string[]
menuCodes: string[]
dataSourceCodes: string[]
}
export interface WizardSeedFileItemDto {
dataField: string
editorType: string
editorOptions: string
editorScript: string
colSpan: number
isRequired: boolean
dbSourceType: number
turkishCaption?: string
englishCaption?: string
captionName?: string
lookupDataSourceType?: number
valueExpr?: string
displayExpr?: string
lookupQuery?: string
}
export interface WizardSeedFileGroupDto {
caption: string
colCount: number
items: WizardSeedFileItemDto[]
}
export interface WizardSeedFileWizardDto extends Omit<ListFormWizardDto, 'groups'> {
groups?: WizardSeedFileGroupDto[]
}
export interface WizardSeedFileDto {
wizard: WizardSeedFileWizardDto
isDeletedField: boolean
isCreatedField: boolean
insertedRecords: WizardInsertedRecordsDto
}

View file

@ -1,4 +1,4 @@
import { ListFormWizardDto, WizardFileInfoDto } from '../proxy/admin/list-form/models'
import { ListFormWizardDto, WizardFileInfoDto, WizardSeedFileDto } from '@/proxy/admin/wizard/models'
import apiService from './api.service'
export const postListFormWizard = (input: ListFormWizardDto) =>
@ -14,6 +14,13 @@ export const getWizardFiles = () =>
url: `/api/app/list-form-wizard/files`,
})
export const getWizardFile = (fileName: string) =>
apiService.fetchData<WizardSeedFileDto>({
method: 'GET',
url: `/api/app/list-form-wizard/file`,
params: { fileName },
})
export const deleteWizardFile = (fileName: string) =>
apiService.fetchData({
method: 'DELETE',

View file

@ -1,12 +1,11 @@
import { FormContainer, Notification, Steps, toast } from '@/components/ui'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { ListFormWizardDto } from '@/proxy/admin/list-form/models'
import { SelectBoxOption } from '@/types/shared'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Form, Formik, FormikProps } from 'formik'
import { useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet'
import { useNavigate } from 'react-router-dom'
import { useLocation, useNavigate } from 'react-router-dom'
import * as Yup from 'yup'
import { getMenus } from '@/services/menu.service'
import { getPermissions } from '@/services/identity.service'
@ -23,12 +22,14 @@ import WizardStep1, {
findRootCode,
} from './WizardStep1'
import WizardStep2 from './WizardStep2'
import WizardStep3, { WizardGroup } from './WizardStep3'
import WizardStep3, { WizardGroup, WizardGroupItem } from './WizardStep3'
import WizardStep4 from './WizardStep4'
import { Container } from '@/components/shared'
import { sqlDataTypeToDbType } from '../edit/options'
import { useStoreActions } from '@/store/store'
import { postListFormWizard } from '@/services/wizard.service'
import { deleteWizardFile, getWizardFile, postListFormWizard } from '@/services/wizard.service'
import { UiLookupDataSourceTypeEnum } from '@/proxy/form/models'
import { ListFormWizardDto } from '@/proxy/admin/wizard/models'
// ─── Formik initial values & validation ──────────────────────────────────────
const initialValues: ListFormWizardDto = {
@ -106,6 +107,13 @@ const listFormValidationSchema = step1ValidationSchema.concat(step2ValidationSch
const Wizard = () => {
const [currentStep, setCurrentStep] = useState(0)
// ── Edit mode ──
const location = useLocation()
const editFileName: string | null = (location.state as any)?.editFileName ?? null
const isEditMode = !!editFileName
const [isLoadingEditData, setIsLoadingEditData] = useState(false)
const editDataLoadedRef = useRef(false)
// ── Data Source ──
const [isLoadingDataSource, setIsLoadingDataSource] = useState(false)
const [dataSourceList, setDataSourceList] = useState<SelectBoxOption[]>([])
@ -259,6 +267,118 @@ const Wizard = () => {
getPermissionGroupList()
}, [])
// ── Load edit data after reference lists are ready ──
useEffect(() => {
if (!isEditMode || !editFileName) return
if (editDataLoadedRef.current) return
if (isLoadingMenu || isLoadingDataSource || isLoadingPermissionGroup) return
if (menuTree.length === 0 && dataSourceList.length === 0) return
editDataLoadedRef.current = true
const loadEditData = async () => {
setIsLoadingEditData(true)
try {
const res = await getWizardFile(editFileName)
const seed = res.data
if (!seed?.wizard) return
const w = seed.wizard
// Populate formik values
formikRef.current?.setValues({
wizardName: w.wizardName ?? '',
listFormCode: w.listFormCode ?? '',
menuCode: w.menuCode ?? '',
isTenant: w.isTenant ?? false,
isBranch: w.isBranch ?? false,
isOrganizationUnit: w.isOrganizationUnit ?? false,
allowAdding: w.allowAdding ?? true,
allowUpdating: w.allowUpdating ?? true,
allowDeleting: w.allowDeleting ?? true,
confirmDelete: w.confirmDelete ?? true,
allowDetail: w.allowDetail ?? false,
defaultLayout: w.defaultLayout ?? 'grid',
grid: w.grid ?? true,
pivot: w.pivot ?? true,
tree: w.tree ?? true,
chart: w.chart ?? true,
gantt: w.gantt ?? true,
scheduler: w.scheduler ?? true,
languageTextMenuEn: w.languageTextMenuEn ?? '',
languageTextMenuTr: w.languageTextMenuTr ?? '',
languageTextTitleEn: w.languageTextTitleEn ?? '',
languageTextTitleTr: w.languageTextTitleTr ?? '',
languageTextDescEn: w.languageTextDescEn ?? '',
languageTextDescTr: w.languageTextDescTr ?? '',
languageTextMenuParentEn: w.languageTextMenuParentEn ?? '',
languageTextMenuParentTr: w.languageTextMenuParentTr ?? '',
permissionGroupName: w.permissionGroupName ?? '',
menuParentCode: w.menuParentCode ?? '',
menuIcon: w.menuIcon ?? '',
dataSourceCode: w.dataSourceCode ?? '',
dataSourceConnectionString: w.dataSourceConnectionString ?? '',
selectCommandType: w.selectCommandType ?? SelectCommandTypeEnum.Table,
selectCommand: w.selectCommand ?? '',
keyFieldName: w.keyFieldName ?? '',
keyFieldDbSourceType: w.keyFieldDbSourceType ?? DbTypeEnum.Int32,
})
// Restore datasource to trigger db objects load
if (w.dataSourceCode) {
setCurrentDataSource(w.dataSourceCode)
}
// Restore groups → WizardGroup[]
if (w.groups && w.groups.length > 0) {
const restoredGroups: WizardGroup[] = w.groups.map((g: any, gi: number) => ({
id: `grp_edit_${gi}_${Date.now()}`,
caption: g.caption ?? '',
colCount: g.colCount ?? 2,
items: (g.items ?? []).map(
(it: any, ii: number) =>
({
id: `${it.dataField}_edit_${ii}_${Date.now()}`,
dataField: it.dataField ?? '',
editorType: it.editorType ?? 'dxTextBox',
editorOptions: it.editorOptions ?? '',
editorScript: it.editorScript ?? '',
colSpan: it.colSpan ?? 1,
isRequired: it.isRequired ?? false,
turkishCaption: it.turkishCaption ?? it.dataField,
englishCaption: it.englishCaption ?? it.dataField,
captionName: it.captionName ?? `App.Listform.ListformField.${it.dataField}`,
lookupDataSourceType:
(it.lookupDataSourceType as UiLookupDataSourceTypeEnum) ??
UiLookupDataSourceTypeEnum.StaticData,
valueExpr: it.valueExpr ?? 'Key',
displayExpr: it.displayExpr ?? 'Name',
lookupQuery: it.lookupQuery ?? '',
}) as WizardGroupItem,
),
}))
setEditingGroups(restoredGroups)
// Restore selectedColumns from all group items
const allFields = new Set(restoredGroups.flatMap((g) => g.items.map((i) => i.dataField)))
setSelectedColumns(allFields)
}
} catch {
toast.push(
<Notification type="danger">
{translate('::App.Listforms.WizardFileLoadError') || 'Failed to load wizard file.'}
</Notification>,
{ placement: 'top-end' },
)
} finally {
setIsLoadingEditData(false)
}
}
loadEditData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editFileName, isLoadingMenu, isLoadingDataSource, isLoadingPermissionGroup])
const navigate = useNavigate()
const formikRef = useRef<FormikProps<ListFormWizardDto>>(null)
@ -365,6 +485,11 @@ const Wizard = () => {
const values = formikRef.current.values
// Edit modunda: önce eski dosyayı sil (DB kayıtlarını temizler)
if (isEditMode && editFileName) {
await deleteWizardFile(editFileName)
}
// 🔴 Önce kayıt işlemi TAMAMLANSIN
await postListFormWizard({
...values,
@ -418,7 +543,7 @@ const Wizard = () => {
<Container>
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate('::' + 'App.Listforms.Wizard')}
title={translate('::' + (isEditMode ? 'App.Listforms.WizardEdit' : 'App.Listforms.Wizard'))}
defaultTitle={APP_NAME}
/>
@ -435,130 +560,138 @@ const Wizard = () => {
</Steps>
</div>
{isLoadingEditData && (
<p className="text-xs text-gray-400 text-center py-4 animate-pulse">
{translate('::App.Platform.Loading') || 'Loading...'}
</p>
)}
<Formik
innerRef={formikRef}
initialValues={{ ...initialValues }}
validationSchema={listFormValidationSchema}
onSubmit={async (values, { setSubmitting }) => {
setSubmitting(true)
onSubmit={async (values, { setSubmitting }) => {
setSubmitting(true)
try {
// 🔴 1. Kaydet (bekle)
await postListFormWizard({ ...values })
try {
// 🔴 1. Kaydet (bekle)
await postListFormWizard({ ...values })
// 🔴 2. Config güncelle (bekle)
await getConfig(true)
// 🔴 2. Config güncelle (bekle)
await getConfig(true)
// 🔴 3. Navigate
navigate(
ROUTES_ENUM.protected.admin.list.replace(':listFormCode', values.listFormCode),
{ replace: true },
)
// 🔴 3. Navigate
navigate(
ROUTES_ENUM.protected.admin.list.replace(':listFormCode', values.listFormCode),
{ replace: true },
)
// 🔴 4. Toast (istersen navigate öncesi de olabilir)
toast.push(
<Notification type="success" duration={2000}>
{translate('::ListForms.FormBilgileriKaydedildi')}
</Notification>,
{ placement: 'top-end' },
)
} catch (error: any) {
toast.push(<Notification title={error.message} type="danger" />, {
placement: 'top-end',
})
} finally {
setSubmitting(false)
}
}}
>
{({ touched, errors, isSubmitting, values }) => (
<Form>
<FormContainer
size={currentStep === 2 ? undefined : currentStep === 3 ? undefined : 'sm'}
>
{/* ─── Step 1: Basic Info ─────────────────────────────── */}
{currentStep === 0 && (
<WizardStep1
values={values}
errors={errors}
touched={touched}
wizardName={values.wizardName}
onWizardNameChange={handleWizardNameChange}
rawMenuItems={rawMenuItems}
menuTree={menuTree}
isLoadingMenu={isLoadingMenu}
onMenuParentChange={handleMenuParentChange}
onClearMenuParent={() => formikRef.current?.setFieldValue('menuParentCode', '')}
onReloadMenu={getMenuList}
permissionGroupList={permissionGroupList}
isLoadingPermissionGroup={isLoadingPermissionGroup}
onNext={handleNext}
translate={translate}
/>
)}
// 🔴 4. Toast (istersen navigate öncesi de olabilir)
toast.push(
<Notification type="success" duration={2000}>
{translate('::ListForms.FormBilgileriKaydedildi')}
</Notification>,
{ placement: 'top-end' },
)
} catch (error: any) {
toast.push(<Notification title={error.message} type="danger" />, {
placement: 'top-end',
})
} finally {
setSubmitting(false)
}
}}
>
{({ touched, errors, isSubmitting, values }) => (
<Form>
<FormContainer
size={currentStep === 2 ? undefined : currentStep === 3 ? undefined : 'sm'}
>
{/* ─── Step 1: Basic Info ─────────────────────────────── */}
{currentStep === 0 && (
<WizardStep1
values={values}
errors={errors}
touched={touched}
wizardName={values.wizardName}
onWizardNameChange={handleWizardNameChange}
rawMenuItems={rawMenuItems}
menuTree={menuTree}
isLoadingMenu={isLoadingMenu}
onMenuParentChange={handleMenuParentChange}
onClearMenuParent={() =>
formikRef.current?.setFieldValue('menuParentCode', '')
}
onReloadMenu={getMenuList}
permissionGroupList={permissionGroupList}
isLoadingPermissionGroup={isLoadingPermissionGroup}
onNext={handleNext}
translate={translate}
/>
)}
{/* ─── Step 2: Data Settings ───────────────────────────── */}
{currentStep === 1 && (
<WizardStep2
values={values}
errors={errors}
touched={touched}
isLoadingDataSource={isLoadingDataSource}
dataSourceList={dataSourceList}
isDataSourceNew={isDataSourceNew}
onDataSourceSelect={setCurrentDataSource}
onDataSourceNewChange={setIsDataSourceNew}
dbObjects={dbObjects}
isLoadingDbObjects={isLoadingDbObjects}
selectCommandColumns={selectCommandColumns}
isLoadingColumns={isLoadingColumns}
selectedColumns={selectedColumns}
onLoadColumns={loadColumns}
onClearColumns={() => {
setSelectCommandColumns([])
setSelectedColumns(new Set())
}}
onToggleColumn={toggleColumn}
onToggleAllColumns={toggleAllColumns}
translate={translate}
onBack={handleBack}
onNext={handleNext2}
/>
)}
{/* ─── Step 2: Data Settings ───────────────────────────── */}
{currentStep === 1 && (
<WizardStep2
values={values}
errors={errors}
touched={touched}
isLoadingDataSource={isLoadingDataSource}
dataSourceList={dataSourceList}
isDataSourceNew={isDataSourceNew}
onDataSourceSelect={setCurrentDataSource}
onDataSourceNewChange={setIsDataSourceNew}
dbObjects={dbObjects}
isLoadingDbObjects={isLoadingDbObjects}
selectCommandColumns={selectCommandColumns}
isLoadingColumns={isLoadingColumns}
selectedColumns={selectedColumns}
onLoadColumns={loadColumns}
onClearColumns={() => {
setSelectCommandColumns([])
setSelectedColumns(new Set())
}}
onToggleColumn={toggleColumn}
onToggleAllColumns={toggleAllColumns}
translate={translate}
onBack={handleBack}
onNext={handleNext2}
/>
)}
{/* ─── Step 3: List Form Fields ───────────────────────────── */}
{currentStep === 2 && (
<WizardStep3
selectedColumns={selectedColumns}
selectCommandColumns={selectCommandColumns}
groups={editingGroups}
onGroupsChange={setEditingGroups}
dbObjects={dbObjects}
isLoadingDbObjects={isLoadingDbObjects}
dsCode={currentDataSource}
translate={translate}
onBack={() => setCurrentStep(1)}
onNext={() => setCurrentStep(3)}
/>
)}
{/* ─── Step 3: List Form Fields ───────────────────────────── */}
{currentStep === 2 && (
<WizardStep3
selectedColumns={selectedColumns}
selectCommandColumns={selectCommandColumns}
groups={editingGroups}
onGroupsChange={setEditingGroups}
dbObjects={dbObjects}
isLoadingDbObjects={isLoadingDbObjects}
dsCode={currentDataSource}
translate={translate}
onBack={() => setCurrentStep(1)}
onNext={() => setCurrentStep(3)}
/>
)}
{/* ─── Step 4: Deploy ───────────────────────────── */}
{currentStep === 3 && (
<WizardStep4
values={values}
wizardName={values.wizardName}
selectedColumns={selectedColumns}
selectCommandColumns={selectCommandColumns}
groups={editingGroups}
translate={translate}
onBack={() => setCurrentStep(2)}
onSubmit={handleDeploy}
/>
)}
</FormContainer>
</Form>
)}
</Formik>
{/* ─── Step 4: Deploy ───────────────────────────── */}
{currentStep === 3 && (
<WizardStep4
values={values}
wizardName={values.wizardName}
selectedColumns={selectedColumns}
selectCommandColumns={selectCommandColumns}
groups={editingGroups}
translate={translate}
onBack={() => setCurrentStep(2)}
onSubmit={handleDeploy}
/>
)}
</FormContainer>
</Form>
)}
</Formik>
</Container>
)
}

View file

@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'
import classNames from 'classnames'
import { Button, Input, Notification, toast } from '@/components/ui'
import Container from '@/components/shared/Container'
import { WizardFileInfoDto } from '@/proxy/admin/list-form/models'
import ConfirmDialog from '@/components/shared/ConfirmDialog'
import {
FaTrash,
FaSync,
@ -11,12 +11,16 @@ import {
FaPlus,
FaExclamationTriangle,
FaSearch,
FaEdit,
} from 'react-icons/fa'
import { FcAcceptDatabase } from 'react-icons/fc'
import { deleteWizardFile, getWizardFiles } from '@/services/wizard.service'
import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { useStoreState } from '@/store/store'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { WizardFileInfoDto } from '@/proxy/admin/wizard/models'
import { UiEvalService } from '@/services/UiEvalService'
// Timestamp formatı: "202605021730" → "2026-05-02 17:30"
const formatTimestamp = (raw: string): string => {
@ -45,6 +49,7 @@ const WizardFileManager = () => {
const [deletingFile, setDeletingFile] = useState<string | null>(null)
const [confirm, setConfirm] = useState<ConfirmState | null>(null)
const [search, setSearch] = useState('')
const [showDbMigrateDialog, setShowDbMigrateDialog] = useState(false)
const filteredFiles = useMemo(() => {
const q = search.trim().toLowerCase()
@ -123,7 +128,7 @@ const WizardFileManager = () => {
<div className="relative">
<FaSearch className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400 text-xs pointer-events-none" />
<Input
size="sm"
size="xs"
className="pl-6 w-44"
placeholder={translate('::App.Platform.Search') || 'Search...'}
value={search}
@ -131,7 +136,7 @@ const WizardFileManager = () => {
/>
</div>
<Button
size="sm"
size="xs"
variant="default"
title={translate('::App.Platform.Refresh') || 'Yenile'}
loading={loading}
@ -140,13 +145,22 @@ const WizardFileManager = () => {
<FaSync />
</Button>
<Button
size="sm"
size="xs"
variant="default"
icon={<FcAcceptDatabase />}
onClick={() => setShowDbMigrateDialog(true)}
title={translate('::App.DbMigrate.StartMessage') || 'Run DB Migration'}
>
{translate('::ListForms.ListForm.DbMigrate') || 'DB Migrate'}
</Button>
<Button
size="xs"
variant="solid"
onClick={() => navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizard)}
className="flex items-center"
>
<FaPlus className="mr-1" />
{translate('::ListForms.ListForm.AddNewRecord') || 'Add New Record'}
{translate('::ListForms.Wizard.AddNewRecord') || 'Add New Record'}
</Button>
</div>
</div>
@ -192,6 +206,20 @@ const WizardFileManager = () => {
<FaExclamationTriangle />
</span>
)}
<Button
size="sm"
variant="plain"
className="text-indigo-500 hover:bg-indigo-50 dark:hover:bg-indigo-900/20"
type="button"
title={translate('::App.Platform.Edit') || 'Edit'}
onClick={() =>
navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizard, {
state: { editFileName: f.fileName },
})
}
>
<FaEdit />
</Button>
<Button
size="sm"
variant="plain"
@ -209,8 +237,30 @@ const WizardFileManager = () => {
))}
</div>
{/* Confirm Dialog */}
{confirm && (
</div>
{/* DB Migrate Confirmation Dialog */}
<ConfirmDialog
isOpen={showDbMigrateDialog}
type="info"
title={translate('::ListForms.ListForm.DbMigrate') || 'DB Migrate'}
cancelText={translate('::Cancel')}
confirmText={translate('::App.Platform.Execute') || 'Çalıştır'}
onCancel={() => setShowDbMigrateDialog(false)}
onClose={() => setShowDbMigrateDialog(false)}
onConfirm={() => {
setShowDbMigrateDialog(false)
UiEvalService.ApiDbMigrate()
}}
>
<p className="text-gray-600 dark:text-gray-400">
{translate('::App.DbMigrate.ConfirmMessage') ||
'Veritabanı migration işlemini başlatmak istediğinize emin misiniz?'}
</p>
</ConfirmDialog>
{/* Delete Confirm Dialog */}
{confirm && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-xl p-6 max-w-sm w-full mx-4">
<div className="flex items-center gap-3 mb-4">
@ -239,7 +289,6 @@ const WizardFileManager = () => {
</div>
</div>
)}
</div>
</Container>
)
}

View file

@ -1,5 +1,4 @@
import { Button, FormItem, Input, Notification, Select, toast } from '@/components/ui'
import { ListFormWizardDto } from '@/proxy/admin/list-form/models'
import { MenuDto } from '@/proxy/menus/models'
import { SelectBoxOption } from '@/types/shared'
import navigationIcon from '@/proxy/menus/navigation-icon.config'
@ -20,6 +19,7 @@ import {
} from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { IconPickerField, MenuAddDialog } from '@/views/shared/MenuAddDialog'
import { ListFormWizardDto } from '@/proxy/admin/wizard/models'
// ─── Types (exported for Wizard.tsx) ─────────────────────────────────────────
@ -256,6 +256,19 @@ function TreeNode({
// ─── MenuTreeInline ───────────────────────────────────────────────────────────
/** Returns all ancestor codes of `code` in the flat rawItems list */
function getAncestorCodes(rawItems: MenuItem[], code: string): Set<string> {
const result = new Set<string>()
let current = code
for (let i = 0; i < 50; i++) {
const item = rawItems.find((r) => r.code === current)
if (!item?.parentCode) break
result.add(item.parentCode)
current = item.parentCode
}
return result
}
interface MenuTreeInlineProps {
value: string
onChange: (code: string) => void
@ -264,6 +277,7 @@ interface MenuTreeInlineProps {
isLoading: boolean
invalid?: boolean
onReload: () => void
initialExpanded?: Set<string>
}
function MenuTreeInline({
@ -274,8 +288,21 @@ function MenuTreeInline({
isLoading,
invalid,
onReload,
initialExpanded,
}: MenuTreeInlineProps) {
const [expanded, setExpanded] = useState<Set<string>>(new Set())
const [expanded, setExpanded] = useState<Set<string>>(() => initialExpanded ?? new Set())
// When value changes (e.g. edit mode loads), expand ancestor nodes
useEffect(() => {
if (!value || rawItems.length === 0) return
const ancestors = getAncestorCodes(rawItems, value)
if (ancestors.size === 0) return
setExpanded((prev) => {
const next = new Set(prev)
ancestors.forEach((c) => next.add(c))
return next
})
}, [value, rawItems])
const [editingCode, setEditingCode] = useState<string | null>(null)
const [editingValue, setEditingValue] = useState('')
const [saving, setSaving] = useState(false)
@ -505,6 +532,7 @@ const WizardStep1 = ({
isLoading={isLoadingMenu}
invalid={!!(errors.menuParentCode && touched.menuParentCode)}
onReload={onReloadMenu}
initialExpanded={values.menuParentCode ? getAncestorCodes(rawMenuItems, values.menuParentCode) : undefined}
/>
)}
</Field>

View file

@ -1,5 +1,4 @@
import { Button, Checkbox, FormItem, Input, Select } from '@/components/ui'
import { ListFormWizardDto } from '@/proxy/admin/list-form/models'
import { SelectCommandTypeEnum } from '@/proxy/form/models'
import type { DatabaseColumnDto, SqlObjectExplorerDto } from '@/proxy/sql-query-manager/models'
import { SelectBoxOption } from '@/types/shared'
@ -7,6 +6,7 @@ import { Field, FieldProps, FormikErrors, FormikTouched } from 'formik'
import CreatableSelect from 'react-select/creatable'
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
import { dbSourceTypeOptions, listFormDefaultLayoutOptions, selectCommandTypeOptions, sqlDataTypeToDbType } from '../edit/options'
import { ListFormWizardDto } from '@/proxy/admin/wizard/models'
// ─── Props ────────────────────────────────────────────────────────────────────

View file

@ -1,5 +1,4 @@
import { Button } from '@/components/ui'
import type { ListFormWizardDto } from '@/proxy/admin/list-form/models'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import { useState } from 'react'
import {
@ -14,6 +13,7 @@ import {
} from 'react-icons/fa'
import { WizardGroup } from './WizardStep3'
import { dbSourceTypeOptions, selectCommandTypeOptions } from '../edit/options'
import { ListFormWizardDto } from '@/proxy/admin/wizard/models'
// ─── Types ────────────────────────────────────────────────────────────────────

View file

@ -6,7 +6,7 @@ import { getDataSources } from '@/services/data-source.service'
import type { DataSourceDto } from '@/proxy/data-source'
import type { SqlQueryExecutionResultDto } from '@/proxy/sql-query-manager/models'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import { FaDatabase, FaPlay, FaFileAlt, FaCopy } from 'react-icons/fa'
import { FaDatabase, FaPlay, FaFileAlt, FaCopy, FaExclamationTriangle } from 'react-icons/fa'
import { FaCheckCircle } from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
import SqlObjectExplorer, { type SqlExplorerSelectedObject } from './SqlObjectExplorer'
@ -1067,7 +1067,7 @@ GO`,
<Button variant="plain" onClick={handleCancelTemplateReplace}>
{translate('::Cancel')}
</Button>
<Button variant="solid" onClick={handleConfirmTemplateReplace}>
<Button variant="solid" onClick={handleConfirmTemplateReplace} icon={<FaExclamationTriangle />} color="red-600">
{translate('::App.Platform.Replace')}
</Button>
</div>

View file

@ -31,6 +31,27 @@ const Chart = React.lazy(() => import('./Chart'))
const GanttView = React.lazy(() => import('./GanttView'))
const SchedulerView = React.lazy(() => import('./SchedulerView'))
function isLayoutValid(dto: GridDto, layout: ListViewLayoutType | undefined): boolean {
if (!layout || layout === 'grid') return true
if (layout === 'pivot') return !!dto.gridOptions?.layoutDto?.pivot
if (layout === 'chart') return !!dto.gridOptions?.layoutDto?.chart
if (layout === 'tree')
return !!(dto.gridOptions?.layoutDto?.tree && dto.gridOptions?.treeOptionDto?.parentIdExpr)
if (layout === 'gantt')
return !!(
dto.gridOptions?.layoutDto?.gantt &&
dto.gridOptions?.ganttOptionDto?.parentIdExpr &&
dto.gridOptions?.ganttOptionDto?.titleExpr
)
if (layout === 'scheduler')
return !!(
dto.gridOptions?.layoutDto?.scheduler &&
dto.gridOptions?.schedulerOptionDto?.textExpr &&
dto.gridOptions?.schedulerOptionDto?.startDateExpr
)
return false
}
const List: React.FC = () => {
const { listFormCode = '' } = useParams()
const [searchParams] = useSearchParams()
@ -69,10 +90,10 @@ const List: React.FC = () => {
if (!gridDto) return
const savedLayout = states.find((s) => s.listFormCode === listFormCode)?.layout
const defaultLayout = gridDto.gridOptions?.layoutDto?.defaultLayout
const candidate = savedLayout ?? defaultLayout
setViewMode(savedLayout ?? defaultLayout)
setViewMode(isLayoutValid(gridDto, candidate) ? candidate : 'grid')
}, [gridDto, states, listFormCode])
/* =======================

View file

@ -262,6 +262,14 @@ const useFilters = ({
})
}
if (grdOpt?.columnOptionDto?.columnAutoWidth) {
menus.push({
text: translate('::ListForms.ListForm.FitColumns'),
id: 'fitColumns',
icon: 'columnchooser',
})
}
//Kullanicinin gridstate kaydi yapmasina izin verilmis ise gridState menu elemanlarini ekle
if (grdOpt?.stateStoringDto?.enabled) {
menus.push({
@ -277,14 +285,6 @@ const useFilters = ({
})
}
if (grdOpt?.columnOptionDto?.columnAutoWidth) {
menus.push({
text: translate('::ListForms.ListForm.FitColumns'),
id: 'fitColumns',
icon: 'columnchooser',
})
}
if (checkPermission(gridDto?.gridOptions.permissionDto.i)) {
menus.push({
text: translate('::ListForms.ListForm.ImportManager'),