-
-
-
-
- {translate('::App.Platform.DeleteAction')}
-
-
- {translate('::App.Listforms.WizardFileDeleteConfirm')}
-
-
-
-
-
-
+
- )}
+
+
+
+
+
+
)
}
diff --git a/ui/src/views/admin/listForm/wizard/WizardStep1.tsx b/ui/src/views/admin/listForm/wizard/WizardStep1.tsx
index 0d8ea56b..ce31de7a 100644
--- a/ui/src/views/admin/listForm/wizard/WizardStep1.tsx
+++ b/ui/src/views/admin/listForm/wizard/WizardStep1.tsx
@@ -66,12 +66,25 @@ export function filterNonLinkNodes(nodes: MenuTreeNode[]): MenuTreeNode[] {
.map((n) => ({ ...n, children: filterNonLinkNodes(n.children) }))
}
+/** code → parentCode haritası; her aramada listeyi taramamak için. */
+function buildParentMap(rawItems: MenuItem[]): Map
{
+ const map = new Map()
+ rawItems.forEach((item) => {
+ if (item.code) map.set(item.code, item.parentCode ?? undefined)
+ })
+ return map
+}
+
export function findRootCode(rawItems: MenuItem[], code: string): string {
+ const parents = buildParentMap(rawItems)
+ const visited = new Set()
let current = code
- for (let i = 0; i < 50; i++) {
- const item = rawItems.find((r) => r.code === current)
- if (!item?.parentCode) return current
- current = item.parentCode
+ // visited: veri bozulsa bile (döngüsel parent) sonsuz döngüye girilmez
+ while (!visited.has(current)) {
+ visited.add(current)
+ const parentCode = parents.get(current)
+ if (!parentCode) return current
+ current = parentCode
}
return current
}
@@ -270,13 +283,14 @@ function TreeNode({
/** Returns all ancestor codes of `code` in the flat rawItems list */
function getAncestorCodes(rawItems: MenuItem[], code: string): Set {
+ const parents = buildParentMap(rawItems)
const result = new Set()
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
+ while (true) {
+ const parentCode = parents.get(current)
+ if (!parentCode || result.has(parentCode)) break
+ result.add(parentCode)
+ current = parentCode
}
return result
}
@@ -375,12 +389,16 @@ function MenuTreeInline({
}
}
- function enrichNode(node: MenuTreeNode): MenuTreeNode & { id?: string } {
- const raw = rawItems.find((i) => i.code === node.code)
- return { ...node, id: raw?.id, children: node.children.map(enrichNode) }
- }
-
- const enrichedNodes = nodes.map(enrichNode)
+ // Menü ağacı her render'da yeniden dolaşılmasın; id eşlemesi tek haritadan yapılır.
+ const enrichedNodes = useMemo(() => {
+ const idByCode = new Map(rawItems.map((item) => [item.code, item.id]))
+ const enrichNode = (node: MenuTreeNode): MenuTreeNode & { id?: string } => ({
+ ...node,
+ id: idByCode.get(node.code),
+ children: node.children.map(enrichNode),
+ })
+ return nodes.map(enrichNode)
+ }, [nodes, rawItems])
const sharedNodeProps = {
expanded,
@@ -483,6 +501,11 @@ const WizardStep1 = ({
const step1Missing = [
!wizardName && translate('::ListForms.Wizard.Step1.WizardName'),
+ !values.menuCode && translate('::ListForms.Wizard.Step1.MenuCode'),
+ // Yup şeması menuOrder >= 1 istiyor; başlangıç değeri 0 olduğu için
+ // burada kontrol edilmezse "Next" aktif görünüp sessizce çalışmıyordu.
+ !(Number(values.menuOrder) >= 1) &&
+ (translate('::ListForms.Wizard.Step1.MenuOrder') || 'Menu Order'),
!values.permissionGroupName && translate('::ListForms.Wizard.Step1.PermissionGroupName'),
!values.languageTextMenuEn && translate('::ListForms.Wizard.Step4.MenuEn'),
!values.languageTextMenuTr && translate('::ListForms.Wizard.Step4.MenuTr'),
diff --git a/ui/src/views/admin/listForm/wizard/WizardStep11.tsx b/ui/src/views/admin/listForm/wizard/WizardStep11.tsx
index bae813d2..07bc27b2 100644
--- a/ui/src/views/admin/listForm/wizard/WizardStep11.tsx
+++ b/ui/src/views/admin/listForm/wizard/WizardStep11.tsx
@@ -1,6 +1,6 @@
import { Button } from '@/components/ui'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
-import React, { useState } from 'react'
+import React, { useMemo, useState } from 'react'
import {
FaArrowLeft,
FaCheckCircle,
@@ -34,7 +34,8 @@ export interface WizardStep11Props {
widgets: WidgetEditDto[]
workflow: WorkflowDto
criteria: ListFormWorkflowCriteriaDto[]
- translate: (key: string) => string
+ // Log satırları {0} gibi parametreler içerdiği için params destekli imza gerekli
+ translate: (key: string, params?: Record) => string
onBack: () => void
onSubmit: () => Promise
}
@@ -48,6 +49,18 @@ interface LogEntry {
detail?: string
}
+/**
+ * Bağlantı cümlesi özet ekranında açıkça gösterilmez; yalnızca kullanıcının
+ * doğru kaynağı seçtiğini anlamasına yetecek kadarı bırakılıp gerisi maskelenir.
+ */
+function maskConnectionString(value?: string) {
+ if (!value) return value
+ return value.replace(
+ /(password|pwd|user id|uid|username|account key|accountkey)\s*=\s*[^;]*/gi,
+ (_match, key: string) => `${key}=••••••`,
+ )
+}
+
// ─── Deploy log steps ─────────────────────────────────────────────────────────
function buildLogSteps(
@@ -175,7 +188,10 @@ const WizardStep11 = ({
const [isDone, setIsDone] = useState(false)
const [hasError, setHasError] = useState(false)
- const steps = buildLogSteps(values, groups, translate)
+ const steps = useMemo(
+ () => buildLogSteps(values, groups, translate),
+ [values, groups, translate],
+ )
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
@@ -308,7 +324,7 @@ const WizardStep11 = ({
/>
+ {/* Bağlantı cümlesi kimlik bilgisi içerebilir; ekranda ve tarayıcı
+ otomatik doldurmasında açıkta bırakılmaz. */}
{
)
}
+/**
+ * DnD ve React key'leri için benzersiz kimlik.
+ * `Date.now()` tek başına yeterli değil: "Tümünü Ekle" aynı milisaniyede
+ * birden fazla item üretip çakışan id'lere yol açıyordu.
+ */
+let idCounter = 0
+const nextId = (prefix: string) => `${prefix}_${Date.now()}_${++idCounter}`
+
function newGroupItem(colName: string, meta?: DatabaseColumnDto): WizardGroupItem {
const sqlType = meta?.dataType ?? ''
return {
- id: `${colName}_${Date.now()}`,
+ id: nextId(colName),
fieldName: colName,
editorType: inferEditorType(sqlType),
editorOptions: '',
@@ -132,7 +140,7 @@ function newGroupItem(colName: string, meta?: DatabaseColumnDto): WizardGroupIte
function newGroup(order: number): WizardGroup {
return {
- id: `grp_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
+ id: nextId('grp'),
caption: `Group ${order}`,
colCount: 2,
items: [],
@@ -864,22 +872,13 @@ const WizardStep3 = ({
// 2. Ensure at least one empty group exists for the user to drag into
const finalGroups =
cleaned.length === 0
- ? [
- {
- id: `grp_default_${Date.now()}`,
- caption: '',
- colCount: 2,
- items: [] as WizardGroupItem[],
- },
- ]
+ ? [{ id: nextId('grp_default'), caption: '', colCount: 2, items: [] as WizardGroupItem[] }]
: cleaned
- // 3. Only update if something actually changed
- const itemsChanged = finalGroups.some(
- (g, i) => g.items.length !== (groups[i]?.items.length ?? -1),
- )
- const countChanged = finalGroups.length !== groups.length
- if (itemsChanged || countChanged) onGroupsChange(finalGroups)
+ // 3. Yalnızca gerçekten bir item düştüyse (veya varsayılan grup eklendiyse) güncelle.
+ // Uzunluk karşılaştırması aynı sayıda ekleme/çıkarma durumunu kaçırıyordu.
+ const removedItem = cleaned.some((g, i) => g.items.length !== groups[i].items.length)
+ if (removedItem || finalGroups.length !== groups.length) onGroupsChange(finalGroups)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedColumns])
diff --git a/ui/src/views/admin/listForm/wizard/WizardStep4.tsx b/ui/src/views/admin/listForm/wizard/WizardStep4.tsx
index 7d3a1b68..19c7fe29 100644
--- a/ui/src/views/admin/listForm/wizard/WizardStep4.tsx
+++ b/ui/src/views/admin/listForm/wizard/WizardStep4.tsx
@@ -39,7 +39,9 @@ const schema = object().shape({
code: string().required(),
})
-const emptySubForm: SubFormDto = {
+// Her yeni kayıt için ayrı nesne üretilir; aksi halde eklenen tüm sub-form'lar
+// aynı relation dizisini / URLSearchParams örneğini paylaşırdı.
+const createEmptySubForm = (): SubFormDto => ({
tabTitle: '',
tabType: SubFormTabTypeEnum.List,
code: '',
@@ -47,7 +49,7 @@ const emptySubForm: SubFormDto = {
relation: [],
tabMode: 'edit',
searchParams: new URLSearchParams(),
-}
+})
function WizardStep4({
subForms,
@@ -243,7 +245,7 @@ function WizardStep4({
>
{dialogIndex !== null && (
diff --git a/ui/src/views/admin/listForm/wizard/WizardStep6.tsx b/ui/src/views/admin/listForm/wizard/WizardStep6.tsx
index a101e030..22aca92e 100644
--- a/ui/src/views/admin/listForm/wizard/WizardStep6.tsx
+++ b/ui/src/views/admin/listForm/wizard/WizardStep6.tsx
@@ -81,14 +81,23 @@ function WizardStep6({
}))
useEffect(() => {
- getUsers(0, 1000).then((response) => {
- setUserList(
- (response.data?.items ?? []).map((user: IdentityUserDto) => ({
- value: user.userName,
- label: `${user.userName} (${user.name} ${user.surname})`,
- })),
- )
- })
+ let cancelled = false
+ getUsers(0, 1000)
+ .then((response) => {
+ if (cancelled) return
+ setUserList(
+ (response.data?.items ?? []).map((user: IdentityUserDto) => ({
+ value: user.userName,
+ label: `${user.userName} (${user.name} ${user.surname})`,
+ })),
+ )
+ })
+ .catch(() => {
+ if (!cancelled) setUserList([])
+ })
+ return () => {
+ cancelled = true
+ }
}, [])
useEffect(() => {
diff --git a/ui/src/views/admin/listForm/workflow/WorkflowCanvas.tsx b/ui/src/views/admin/listForm/workflow/WorkflowCanvas.tsx
index 573dd1f6..35523254 100644
--- a/ui/src/views/admin/listForm/workflow/WorkflowCanvas.tsx
+++ b/ui/src/views/admin/listForm/workflow/WorkflowCanvas.tsx
@@ -298,7 +298,8 @@ function FlowNode({
variant="plain"
shape="none"
className={classNames(
- 'absolute z-40 grid !h-32 !w-44 touch-none content-start justify-items-start gap-1 rounded-lg border-2 border-[#667085] !bg-white !p-2.5 text-left text-slate-700 shadow-lg hover:!bg-white active:!bg-white dark:border-gray-600 dark:!bg-gray-900 dark:text-gray-200 dark:hover:!bg-gray-900 dark:active:!bg-gray-900',
+ // Metin renkleri `!` ile: Button variant'ının kendi text rengini ezmesi gerekiyor.
+ 'absolute z-40 grid !h-32 !w-44 touch-none content-start justify-items-start gap-1 rounded-lg border-2 border-[#667085] !bg-white !p-2.5 text-left !text-slate-700 shadow-lg hover:!bg-white active:!bg-white dark:border-gray-600 dark:!bg-gray-900 dark:!text-gray-200 dark:hover:!bg-gray-900 dark:active:!bg-gray-900',
{
'border-blue-600 outline outline-[3px] outline-blue-600/20': selected,
'border-green-600 !bg-green-50 shadow-[0_0_0_4px_rgba(22,163,74,0.18),0_10px_24px_rgba(22,101,52,0.14)] hover:!bg-green-50 active:!bg-green-50 dark:!bg-green-900/20 dark:hover:!bg-green-900/20 dark:active:!bg-green-900/20':
diff --git a/ui/src/views/admin/listForm/workflow/WorkflowDesigner.tsx b/ui/src/views/admin/listForm/workflow/WorkflowDesigner.tsx
index 59503876..958330ba 100644
--- a/ui/src/views/admin/listForm/workflow/WorkflowDesigner.tsx
+++ b/ui/src/views/admin/listForm/workflow/WorkflowDesigner.tsx
@@ -51,6 +51,24 @@ const designerIconButtonClass =
const designerTabClass = '!h-auto min-h-8 rounded-md border !px-3 py-1.5 transition-colors'
+/**
+ * Button bileşeni variant'a göre kendi metin rengini basıyor
+ * (`solid` → `text-white`, `plain` → `text-gray-600`). Aynı özgüllükteki
+ * yardımcı sınıflar CSS sırasına göre eziliyor ve beyaz zeminde beyaz yazı
+ * oluşuyordu. Bu yüzden metin renkleri `!` ile önceliklendirilir.
+ */
+const designerNeutralColorClass =
+ 'border-gray-300 !bg-white !text-slate-700 hover:!bg-gray-50 dark:border-gray-700 dark:!bg-gray-800 dark:!text-gray-200 dark:hover:!bg-gray-700'
+
+const designerAccentColorClass =
+ 'border-blue-600 !bg-white !text-blue-600 hover:!bg-blue-50 dark:border-blue-500 dark:!bg-gray-800 dark:!text-blue-300 dark:hover:!bg-blue-900/30'
+
+const designerTabActiveClass =
+ 'border-blue-700 !bg-blue-700 !text-white shadow-sm dark:border-blue-500 dark:!bg-blue-600 dark:!text-white'
+
+const designerTabInactiveClass =
+ 'border-gray-200 !bg-white !text-slate-600 hover:!bg-gray-50 dark:border-gray-700 dark:!bg-gray-800 dark:!text-gray-300 dark:hover:!bg-gray-700'
+
export function WorkflowDesigner({
busy,
canvasRef,
@@ -177,10 +195,7 @@ function DesignerToolbar({
type="button"
variant="solid"
shape="round"
- className={classNames(
- designerButtonClass,
- 'border-gray-300 !bg-white text-slate-700 hover:!bg-gray-50 dark:border-gray-700 dark:!bg-gray-800 dark:text-gray-200 dark:hover:!bg-gray-700',
- )}
+ className={classNames(designerButtonClass, designerNeutralColorClass)}
disabled={busy}
title="Demo akışı yükle"
onClick={onResetDemo}
@@ -192,10 +207,7 @@ function DesignerToolbar({
type="button"
variant="solid"
shape="round"
- className={classNames(
- designerButtonClass,
- 'border-blue-600 !bg-white text-blue-600 hover:!bg-blue-50 dark:border-blue-500 dark:!bg-gray-800 dark:text-blue-300 dark:hover:!bg-blue-900/30',
- )}
+ className={classNames(designerButtonClass, designerAccentColorClass)}
disabled={busy || currentCriteria.length === 0}
title="Düğümleri okunabilir şekilde yerleştir"
onClick={onFitLayout}
@@ -207,10 +219,7 @@ function DesignerToolbar({
type="button"
variant="solid"
shape="round"
- className={classNames(
- designerIconButtonClass,
- 'border-blue-600 !bg-white text-blue-600 hover:!bg-blue-50 dark:border-blue-500 dark:!bg-gray-800 dark:text-blue-300 dark:hover:!bg-blue-900/30',
- )}
+ className={classNames(designerIconButtonClass, designerAccentColorClass)}
title="Yakınlaştır"
onClick={onZoomIn}
>
@@ -220,10 +229,7 @@ function DesignerToolbar({
type="button"
variant="solid"
shape="round"
- className={classNames(
- designerIconButtonClass,
- 'border-blue-600 !bg-white text-blue-600 hover:!bg-blue-50 dark:border-blue-500 dark:!bg-gray-800 dark:text-blue-300 dark:hover:!bg-blue-900/30',
- )}
+ className={classNames(designerIconButtonClass, designerAccentColorClass)}
title="Uzaklaştır"
onClick={onZoomOut}
>
@@ -240,10 +246,7 @@ function DesignerToolbar({
type="button"
variant="solid"
shape="round"
- className={classNames(
- designerButtonClass,
- 'border-blue-600 !bg-white text-blue-600 hover:!bg-blue-50 dark:border-blue-500 dark:!bg-gray-800 dark:text-blue-300 dark:hover:!bg-blue-900/30',
- )}
+ className={classNames(designerButtonClass, designerAccentColorClass)}
disabled={busy}
onClick={() => onAddCriteria(option.value)}
>
@@ -274,9 +277,7 @@ function DesignerTabs({
role="tab"
className={classNames(
designerTabClass,
- activeTab === 'flow'
- ? 'border-blue-700 !bg-blue-700 text-white shadow-sm dark:border-blue-500 dark:!bg-blue-600'
- : 'border-gray-200 !bg-white text-slate-600 hover:!bg-gray-50 dark:border-gray-700 dark:!bg-gray-800 dark:text-gray-300 dark:hover:!bg-gray-700',
+ activeTab === 'flow' ? designerTabActiveClass : designerTabInactiveClass,
)}
onClick={() => onChange('flow')}
>
@@ -289,9 +290,7 @@ function DesignerTabs({
role="tab"
className={classNames(
designerTabClass,
- activeTab === 'criteria'
- ? 'border-blue-700 !bg-blue-700 text-white shadow-sm dark:border-blue-500 dark:!bg-blue-600'
- : 'border-gray-200 !bg-white text-slate-600 hover:!bg-gray-50 dark:border-gray-700 dark:!bg-gray-800 dark:text-gray-300 dark:hover:!bg-gray-700',
+ activeTab === 'criteria' ? designerTabActiveClass : designerTabInactiveClass,
)}
onClick={() => onChange('criteria')}
>
diff --git a/ui/src/views/shared/MenuAddDialog.tsx b/ui/src/views/shared/MenuAddDialog.tsx
index 23b9db50..97eebf88 100644
--- a/ui/src/views/shared/MenuAddDialog.tsx
+++ b/ui/src/views/shared/MenuAddDialog.tsx
@@ -6,6 +6,8 @@ import navigationIcon from '@/proxy/menus/navigation-icon.config'
import { useEffect, useRef, useState } from 'react'
import { FaChevronDown, FaPlus } from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
+import { useConfig } from '@/components/ui/ConfigProvider'
+import { useForm } from '@/components/ui/Form/context'
const menuService = new MenuService()
@@ -26,6 +28,12 @@ export function IconPickerField({ value, onChange, invalid }: IconPickerFieldPro
const [limit, setLimit] = useState(ICON_PAGE_SIZE)
const wrapperRef = useRef(null)
+ // Tetikleyici yüksekliği, komşu Input/Select alanlarıyla aynı olsun diye
+ // Input bileşeniyle birebir aynı kaynaktan (form context → global config) okunur.
+ const formSize = useForm()?.size
+ const { controlSize: defaultControlSize } = useConfig()
+ const controlSize = formSize ?? defaultControlSize
+
const SelectedIcon = value ? navigationIcon[value] : null
const filtered = search.trim()
? ALL_ICON_ENTRIES.filter(([key]) => key.toLowerCase().includes(search.toLowerCase()))
@@ -48,27 +56,33 @@ export function IconPickerField({ value, onChange, invalid }: IconPickerFieldPro
return (
+ {/*
+ Tetikleyici, platformun `.input` sınıfını kullanır; böylece kenarlık, köşe
+ yarıçapı, dolgu ve açık/koyu tema arka planı diğer form alanlarıyla birebir
+ aynı olur. Metin renkleri `!` ile verilir: Button bileşeni variant'ına göre
+ kendi `text-*` sınıfını basıyor ve aynı özgüllükte olduğu için eziyordu.
+ */}