diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json index 89e0153..11e405f 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json @@ -1416,6 +1416,18 @@ "en": "List Form Settings", "tr": "Liste Formu Ayarları" }, + { + "resourceName": "Platform", + "key": "ListForms.Wizard.ListFormFields", + "en": "List Form Fields", + "tr": "Liste Formu Alanları" + }, + { + "resourceName": "Platform", + "key": "ListForms.Wizard.Deploy", + "en": "Deploy", + "tr": "Dağıtım" + }, { "resourceName": "Platform", "key": "App.Listforms.DataSource", diff --git a/ui/src/views/admin/listForm/Wizard.tsx b/ui/src/views/admin/listForm/Wizard.tsx index 6589679..8f72a7b 100644 --- a/ui/src/views/admin/listForm/Wizard.tsx +++ b/ui/src/views/admin/listForm/Wizard.tsx @@ -1,30 +1,20 @@ -import Container from '@/components/shared/Container' -import { - Button, - FormContainer, - FormItem, - Input, - Notification, - Select, - Steps, - toast, -} from '@/components/ui' +import { Button, 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 { Field, FieldProps, Form, Formik, FormikProps } from 'formik' +import { Form, Formik, FormikProps } from 'formik' import { useEffect, useRef, useState } from 'react' import { Helmet } from 'react-helmet' import { useNavigate } from 'react-router-dom' -import CreatableSelect from 'react-select/creatable' import * as Yup from 'yup' -import { dbSourceTypeOptions, selectCommandTypeOptions } from './edit/options' import { getMenus } from '@/services/menu.service' import { getPermissions } from '@/services/identity.service' import { DbTypeEnum, SelectCommandTypeEnum } from '@/proxy/form/models' import { postListFormWizard } from '@/services/admin/list-form.service' import { getDataSources } from '@/services/data-source.service' +import { sqlObjectManagerService } from '@/services/sql-query-manager.service' +import type { SqlObjectExplorerDto, DatabaseColumnDto } from '@/proxy/sql-query-manager/models' import { APP_NAME } from '@/constants/app.constant' import { MenuItem } from '@/proxy/menus/menu' import WizardStep1, { @@ -33,10 +23,10 @@ import WizardStep1, { filterNonLinkNodes, findRootCode, } from './WizardStep1' - +import WizardStep2, { sqlDataTypeToDbType } from './WizardStep2' +import { Container } from '@/components/shared' // ─── Formik initial values & validation ────────────────────────────────────── - const initialValues: ListFormWizardDto = { listFormCode: '', menuCode: '', @@ -73,7 +63,7 @@ const step2ValidationSchema = Yup.object().shape({ languageTextTitleTr: Yup.string(), languageTextDescEn: Yup.string(), languageTextDescTr: Yup.string(), - dataSourceCode: Yup.string(), + dataSourceCode: Yup.string().required(), dataSourceConnectionString: Yup.string(), selectCommandType: Yup.string().required(), selectCommand: Yup.string().required(), @@ -93,6 +83,73 @@ const Wizard = () => { const [isLoadingDataSource, setIsLoadingDataSource] = useState(false) const [dataSourceList, setDataSourceList] = useState([]) const [isDataSourceNew, setIsDataSourceNew] = useState(false) + + // ── DB Objects (Tables / SP / Views / Functions) ── + const [dbObjects, setDbObjects] = useState(null) + const [isLoadingDbObjects, setIsLoadingDbObjects] = useState(false) + const [currentDataSource, setCurrentDataSource] = useState('') + + const loadDbObjects = async (dsCode: string) => { + if (!dsCode) { + setDbObjects(null) + return + } + setIsLoadingDbObjects(true) + try { + const res = await sqlObjectManagerService.getAllObjects(dsCode) + setDbObjects(res.data) + } catch { + setDbObjects(null) + } finally { + setIsLoadingDbObjects(false) + } + } + + useEffect(() => { + loadDbObjects(currentDataSource) + }, [currentDataSource]) + + // ── Column List for KeyFieldName & Column Selector ── + const [selectCommandColumns, setSelectCommandColumns] = useState([]) + const [isLoadingColumns, setIsLoadingColumns] = useState(false) + const [selectedColumns, setSelectedColumns] = useState>(new Set()) + + const loadColumns = async (dsCode: string, schema: string, name: string) => { + if (!dsCode || !name) { + setSelectCommandColumns([]) + setSelectedColumns(new Set()) + return + } + setIsLoadingColumns(true) + try { + const res = await sqlObjectManagerService.getTableColumns(dsCode, schema, name) + const cols = res.data ?? [] + setSelectCommandColumns(cols) + setSelectedColumns(new Set(cols.map((c) => c.columnName))) + // Auto-select first column as key field + if (cols.length > 0) { + const first = cols[0] + formikRef.current?.setFieldValue('keyFieldName', first.columnName) + formikRef.current?.setFieldValue('keyFieldDbSourceType', sqlDataTypeToDbType(first.dataType)) + } + } catch { + setSelectCommandColumns([]) + setSelectedColumns(new Set()) + } finally { + setIsLoadingColumns(false) + } + } + + const toggleColumn = (col: string) => + setSelectedColumns((prev) => { + const next = new Set(prev) + next.has(col) ? next.delete(col) : next.add(col) + return next + }) + + const toggleAllColumns = (all: boolean) => + setSelectedColumns(all ? new Set(selectCommandColumns.map((c) => c.columnName)) : new Set()) + const getDataSourceList = async () => { setIsLoadingDataSource(true) const response = await getDataSources() @@ -212,6 +269,20 @@ const Wizard = () => { const handleBack = () => setCurrentStep(0) + const handleNext2 = async () => { + if (!formikRef.current) return + const errors = await formikRef.current.validateForm() + const step2Fields = Object.keys(step2ValidationSchema.fields) + const hasStep2Errors = step2Fields.some((f) => errors[f as keyof ListFormWizardDto]) + const touchedStep2 = step2Fields.reduce( + (acc, key) => ({ ...acc, [key]: true }), + {} as Record, + ) + await formikRef.current.setTouched({ ...formikRef.current.touched, ...touchedStep2 }) + if (hasStep2Errors) return + setCurrentStep(2) + } + return ( { + + -
- { - setSubmitting(true) - try { - await postListFormWizard({ ...values }) - toast.push( - - {translate('::ListForms.FormBilgileriKaydedildi')} - , - { placement: 'top-end' }, + { + setSubmitting(true) + try { + await postListFormWizard({ ...values }) + toast.push( + + {translate('::ListForms.FormBilgileriKaydedildi')} + , + { placement: 'top-end' }, + ) + setSubmitting(false) + setTimeout(() => { + navigate( + ROUTES_ENUM.protected.saas.listFormManagement.edit.replace( + ':listFormCode', + values.listFormCode, + ), ) - setSubmitting(false) - setTimeout(() => { - navigate( - ROUTES_ENUM.protected.saas.listFormManagement.edit.replace( - ':listFormCode', - values.listFormCode, - ), - ) - }, 500) - } catch (error: any) { - toast.push(, { - placement: 'top-end', - }) - } - }} - > - {({ touched, errors, isSubmitting, values }) => ( -
- - {/* ─── Step 1: Basic Info ─────────────────────────────── */} - {currentStep === 0 && ( - formikRef.current?.setFieldValue('menuParentCode', '')} - onReloadMenu={getMenuList} - permissionGroupList={permissionGroupList} - isLoadingPermissionGroup={isLoadingPermissionGroup} - onNext={handleNext} - translate={translate} - /> - )} + }, 500) + } catch (error: any) { + toast.push(, { + placement: 'top-end', + }) + } + }} + > + {({ touched, errors, isSubmitting, values }) => ( + + + {/* ─── Step 1: Basic Info ─────────────────────────────── */} + {currentStep === 0 && ( + formikRef.current?.setFieldValue('menuParentCode', '')} + onReloadMenu={getMenuList} + permissionGroupList={permissionGroupList} + isLoadingPermissionGroup={isLoadingPermissionGroup} + onNext={handleNext} + translate={translate} + /> + )} - {/* ─── Step 2: Data Settings ───────────────────────────── */} - {currentStep === 1 && ( - <> - {/* ListForm Code */} - - Auto-derived from Wizard Name, editable - - } - > - - + {/* ─── Step 2: Data Settings ───────────────────────────── */} + {currentStep === 1 && ( + { + setSelectCommandColumns([]) + setSelectedColumns(new Set()) + }} + onToggleColumn={toggleColumn} + onToggleAllColumns={toggleAllColumns} + translate={translate} + onBack={handleBack} + onNext={handleNext2} + /> + )} -
- - - - - - -
+ {/* ─── Step 3: List Form Fields ───────────────────────────── */} + {currentStep === 2 && ( +
+ + +
+ )} -
- - - - - - -
- -
- - - {({ field, form }: FieldProps) => ( - o.value === field.value, - )} - onChange={(o) => form.setFieldValue(field.name, o?.value)} - /> - )} - - - - - - -
- -
- - - - - - - {({ field, form }: FieldProps) => ( - setForm((p) => ({ ...p, code: e.target.value }))} placeholder="App.MyMenu" - className="h-11 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" /> + + setForm((p) => ({ ...p, code: e.target.value }))} + placeholder="App.MyMenu" + className="h-11 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" + />
- - setForm((p) => ({ ...p, displayName: e.target.value }))} placeholder="My Menu" - className="h-11 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" /> + + setForm((p) => ({ ...p, displayName: e.target.value }))} + placeholder="My Menu" + className="h-11 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" + />
- setForm((p) => ({ ...p, icon: key }))} /> + setForm((p) => ({ ...p, icon: key }))} + />
- - + +
- - setForm((p) => ({ ...p, order: Number(e.target.value) }))} - className="h-11 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" /> + + setForm((p) => ({ ...p, order: Number(e.target.value) }))} + className="h-11 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" + />
- - setForm((p) => ({ ...p, shortName: e.target.value }))} placeholder="My Menu (short)" - className="h-11 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" /> + + setForm((p) => ({ ...p, shortName: e.target.value }))} + placeholder="My Menu (short)" + className="h-11 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" + />
- - + +
@@ -510,152 +650,212 @@ export interface WizardStep1Props { } const WizardStep1 = ({ - values, errors, touched, - wizardName, onWizardNameChange, - rawMenuItems, menuTree, isLoadingMenu, - onMenuParentChange, onClearMenuParent, onReloadMenu, - permissionGroupList, isLoadingPermissionGroup, - onNext, translate, + values, + errors, + touched, + wizardName, + onWizardNameChange, + rawMenuItems, + menuTree, + isLoadingMenu, + onMenuParentChange, + onClearMenuParent, + onReloadMenu, + permissionGroupList, + isLoadingPermissionGroup, + onNext, + translate, }: WizardStep1Props) => { const [menuDialogOpen, setMenuDialogOpen] = useState(false) const [menuDialogParentCode, setMenuDialogParentCode] = useState('') const [menuDialogInitialOrder, setMenuDialogInitialOrder] = useState(999) return ( - <> +
{/* Wizard Name */} Used to generate ListForm Code and Menu Code} + extra={ + + Used to generate ListForm Code and Menu Code + + } > onWizardNameChange(e.target.value)} /> - {/* Menu Parent */} - - - {values.menuParentCode && ( - - )} -
- } - > - - {() => ( - + {/* Col 1 */} +
+ {/* Menu Parent */} + + + {values.menuParentCode && ( + + )} +
+ } + > + + {() => ( + + )} + + + + setMenuDialogOpen(false)} + initialParentCode={menuDialogParentCode} + initialOrder={menuDialogInitialOrder} + rawItems={rawMenuItems} + onSaved={onReloadMenu} + /> + + + {/* Col 2 */} +
+ {/* Menu Code */} + Auto-derived, editable} + > + - )} - - + - setMenuDialogOpen(false)} - initialParentCode={menuDialogParentCode} - initialOrder={menuDialogInitialOrder} - rawItems={rawMenuItems} - onSaved={onReloadMenu} - /> - - {/* Menu Code */} - Auto-derived, editable} - > - - - - - - - - - - - - {/* Permission Group Name */} - - - {({ field, form }: FieldProps) => ( - o.value === values.permissionGroupName, + ) ?? { + label: values.permissionGroupName, + value: values.permissionGroupName, + }) + : null + } + onChange={(option) => form.setFieldValue(field.name, option?.value)} + /> + )} + + +
+ + + {/* ─── Fixed Footer ─────────────────────────────── */} +
+
+ +
+
+ ) } diff --git a/ui/src/views/admin/listForm/WizardStep2.tsx b/ui/src/views/admin/listForm/WizardStep2.tsx new file mode 100644 index 0000000..3517d7e --- /dev/null +++ b/ui/src/views/admin/listForm/WizardStep2.tsx @@ -0,0 +1,462 @@ +import { Button, FormItem, Input, Select } from '@/components/ui' +import { ListFormWizardDto } from '@/proxy/admin/list-form/models' +import { DbTypeEnum, SelectCommandTypeEnum } from '@/proxy/form/models' +import type { DatabaseColumnDto, SqlObjectExplorerDto } from '@/proxy/sql-query-manager/models' +import { SelectBoxOption } from '@/types/shared' +import { dbSourceTypeOptions, selectCommandTypeOptions } from './edit/options' +import { Field, FieldProps, FormikErrors, FormikTouched } from 'formik' +import CreatableSelect from 'react-select/creatable' + +// ─── SQL dataType → DbTypeEnum mapper ──────────────────────────────────────── + +export function sqlDataTypeToDbType(sqlType: string): DbTypeEnum { + const t = sqlType + .toLowerCase() + .replace(/\s*\(.*\)/, '') + .trim() + if (['int', 'integer', 'int32'].includes(t)) return DbTypeEnum.Int32 + if (['bigint', 'int64'].includes(t)) return DbTypeEnum.Int64 + if (['smallint', 'int16'].includes(t)) return DbTypeEnum.Int16 + if (['tinyint', 'byte'].includes(t)) return DbTypeEnum.Byte + if (['bit', 'boolean', 'bool'].includes(t)) return DbTypeEnum.Boolean + if (['float', 'real', 'double', 'double precision'].includes(t)) return DbTypeEnum.Double + if (['decimal', 'numeric', 'money', 'smallmoney'].includes(t)) return DbTypeEnum.Decimal + if (['uniqueidentifier'].includes(t)) return DbTypeEnum.Guid + if (['datetime2', 'smalldatetime', 'datetime'].includes(t)) return DbTypeEnum.DateTime + if (['date'].includes(t)) return DbTypeEnum.Date + if (['time'].includes(t)) return DbTypeEnum.Time + if (['datetimeoffset'].includes(t)) return DbTypeEnum.DateTimeOffset + if (['nvarchar', 'varchar', 'char', 'nchar', 'text', 'ntext', 'string'].includes(t)) + return DbTypeEnum.String + if (['xml'].includes(t)) return DbTypeEnum.Xml + if (['binary', 'varbinary', 'image'].includes(t)) return DbTypeEnum.Binary + return DbTypeEnum.String +} + +// ─── Props ──────────────────────────────────────────────────────────────────── + +export interface WizardStep2Props { + values: ListFormWizardDto + errors: FormikErrors + touched: FormikTouched + // Data Source + isLoadingDataSource: boolean + dataSourceList: SelectBoxOption[] + isDataSourceNew: boolean + onDataSourceSelect: (value: string) => void + onDataSourceNewChange: (isNew: boolean) => void + // DB Objects + dbObjects: SqlObjectExplorerDto | null + isLoadingDbObjects: boolean + // Columns + selectCommandColumns: DatabaseColumnDto[] + isLoadingColumns: boolean + selectedColumns: Set + onLoadColumns: (dsCode: string, schema: string, name: string) => void + onClearColumns: () => void + onToggleColumn: (col: string) => void + onToggleAllColumns: (all: boolean) => void + // Navigation + translate: (key: string) => string + onBack: () => void + onNext: () => void +} + +// ─── WizardStep2 ────────────────────────────────────────────────────────────── + +const WizardStep2 = ({ + values, + errors, + touched, + isLoadingDataSource, + dataSourceList, + isDataSourceNew, + onDataSourceSelect, + onDataSourceNewChange, + dbObjects, + isLoadingDbObjects, + selectCommandColumns, + isLoadingColumns, + selectedColumns, + onLoadColumns, + onClearColumns, + onToggleColumn, + onToggleAllColumns, + translate, + onBack, + onNext, +}: WizardStep2Props) => { + return ( +
+ {/* ListForm Code + Data Source */} +
+ + Auto-derived from Wizard Name, editable + + } + > + + + + + + {({ field, form }: FieldProps) => ( + { + if (!option) { + form.setFieldValue(field.name, '') + onClearColumns() + return + } + form.setFieldValue(field.name, option.value) + const type = option.__isNew__ + ? SelectCommandTypeEnum.Query + : (option.__type ?? SelectCommandTypeEnum.Query) + form.setFieldValue('selectCommandType', type) + form.setFieldValue('keyFieldName', '') + form.setFieldTouched('keyFieldName', false) + if (!option.__isNew__ && option.__schema != null && option.__rawName) { + onLoadColumns(values.dataSourceCode, option.__schema, option.__rawName) + } else { + onClearColumns() + } + }} + onCreateOption={(inputValue: string) => { + form.setFieldValue(field.name, inputValue) + form.setFieldValue('selectCommandType', SelectCommandTypeEnum.Query) + form.setFieldValue('keyFieldName', '') + form.setFieldTouched('keyFieldName', false) + onClearColumns() + }} + /> + ) + }} + + + + + {dbSourceTypeOptions.find((o: any) => o.value === values.keyFieldDbSourceType) + ?.label ?? String(values.keyFieldDbSourceType)} + + ) : selectCommandColumns.length === 0 && !isLoadingColumns ? ( + + Select Command seçince sütunlar yüklenir + + ) : null + } + > + + {({ field, form }: FieldProps) => ( + onToggleColumn(col.columnName)} + className="w-3.5 h-3.5 accent-indigo-500 shrink-0" + /> + + + {col.columnName} + + + {col.dataType} + {col.isNullable ? '?' : ''} + + + + ))} +
+
+ )} + + + + {/* ─── Fixed Footer ─────────────────────────────── */} +
+
+ + +
+
+ + ) +} + +export default WizardStep2