Wizard ve Component Manager List ve Card görünümleri

This commit is contained in:
Sedat ÖZTÜRK 2026-08-20 18:20:49 +03:00
parent 4c09de9ad0
commit 90a66562e7
6 changed files with 610 additions and 325 deletions

View file

@ -10854,6 +10854,48 @@
"tr": "Görüntüle",
"en": "View"
},
{
"resourceName": "Platform",
"key": "App.Platform.CardView",
"tr": "Kart Görünümü",
"en": "Card View"
},
{
"resourceName": "Platform",
"key": "App.Platform.ListView",
"tr": "Liste Görünümü",
"en": "List View"
},
{
"resourceName": "Platform",
"key": "App.Platform.Name",
"tr": "Ad",
"en": "Name"
},
{
"resourceName": "Platform",
"key": "App.Platform.Description",
"tr": "Açıklama",
"en": "Description"
},
{
"resourceName": "Platform",
"key": "App.Platform.Actions",
"tr": "İşlemler",
"en": "Actions"
},
{
"resourceName": "Platform",
"key": "App.Platform.Dependencies",
"tr": "Bağımlılıklar",
"en": "Dependencies"
},
{
"resourceName": "Platform",
"key": "App.Platform.RoutePath",
"tr": "Rota",
"en": "Route Path"
},
{
"resourceName": "Platform",
"key": "App.Platform.Manage2",

View file

@ -18,6 +18,8 @@ import {
FaExternalLinkAlt,
FaFileExport,
FaFileImport,
FaTh,
FaList,
} from 'react-icons/fa'
import DbMigrateButton from '@/components/shared/DbMigrateButton'
import {
@ -28,7 +30,8 @@ import {
} from '@/services/wizard.service'
import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { useStoreState } from '@/store/store'
import { useStoreActions, useStoreState } from '@/store/store'
import type { ListViewLayoutType } from '../edit/types'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { WizardComponentKindEnum, WizardFileInfoDto } from '@/proxy/admin/wizard/models'
import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config'
@ -36,6 +39,9 @@ import { usePermission } from '@/utils/hooks/usePermission'
import { WIZARD_PERMISSION } from '@/constants/permission.constant'
import WizardImportDialog from './WizardImportDialog'
/** Layout tercihi diger listelerle ayni yerde (admin.lists.states) saklanir. */
const VIEW_STATE_CODE = 'developerkit-wizardmanager'
interface ConfirmState {
fileName: string
wizardName: string
@ -57,6 +63,13 @@ const WizardFileManager = () => {
const [importFile, setImportFile] = useState<File | null>(null)
const importInputRef = useRef<HTMLInputElement>(null)
const { states } = useStoreState((state) => state.admin.lists)
const { setStates } = useStoreActions((a) => a.admin.lists)
const viewMode: ListViewLayoutType =
states.find((s) => s.listFormCode === VIEW_STATE_CODE)?.layout === 'grid' ? 'grid' : 'card'
const setViewMode = (layout: ListViewLayoutType) =>
setStates({ listFormCode: VIEW_STATE_CODE, layout })
// Butonlar yalnizca gizlenir; asil kontrol ListFormWizardAppService'te yapilir.
const { checkPermission } = usePermission()
const canCreate = checkPermission(WIZARD_PERMISSION.CREATE)
@ -152,6 +165,72 @@ const WizardFileManager = () => {
}
}
const renderActions = (f: WizardFileInfoDto, className: string) => (
<div className={className}>
{!f.hasInsertedRecords && (
<span
title={translate('::App.ListForm.WizardFileNoTrackedRecords')}
className="text-yellow-500 text-xs flex items-center gap-1"
>
<FaExclamationTriangle />
</span>
)}
{canUpdate && (
<Button
type="button"
variant="solid"
color="blue-600"
title={translate('::App.Platform.Edit')}
onClick={() =>
navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizard, {
state: { editFileName: f.fileName },
})
}
>
<FaEdit className="w-4 h-4" />
</Button>
)}
{/* Menünün açtığı adres yeni sekmede açılır; adres yoksa buton anlamsızdır. */}
{f.menuUrl && (
<Button
type="button"
variant="solid"
color="gray-600"
title={translate('::App.Platform.OpenUrl')}
onClick={() => window.open(f.menuUrl, '_blank', 'noopener,noreferrer')}
>
<FaExternalLinkAlt className="w-4 h-4" />
</Button>
)}
{canExport && (
<Button
type="button"
variant="solid"
color="green-600"
title={translate('::App.ListForm.WizardFileExport')}
loading={exportingFile === f.fileName}
onClick={() => handleExport(f)}
>
<FaFileExport className="w-4 h-4" />
</Button>
)}
{canDelete && (
<Button
type="button"
variant="solid"
color="red-600"
title={translate('::App.Platform.Delete')}
loading={deletingFile === f.fileName}
onClick={() =>
setConfirm({ fileName: f.fileName, wizardName: f.wizardName || f.fileName })
}
>
<FaTrash className="w-4 h-4" />
</Button>
)}
</div>
)
return (
<Container>
{/* ── Header ─────────────────────────────────────────────── */}
@ -227,6 +306,24 @@ const WizardFileManager = () => {
</span>
</Button>
)}
<div className="flex items-center gap-1">
<Button
size="sm"
type="button"
icon={<FaTh />}
variant={viewMode === 'card' ? 'solid' : 'default'}
title={translate('::App.Platform.CardView')}
onClick={() => setViewMode('card')}
/>
<Button
size="sm"
type="button"
icon={<FaList />}
variant={viewMode === 'grid' ? 'solid' : 'default'}
title={translate('::App.Platform.ListView')}
onClick={() => setViewMode('grid')}
/>
</div>
</div>
</div>
@ -246,6 +343,82 @@ const WizardFileManager = () => {
</p>
)}
{viewMode === 'card' ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
{filteredFiles.map((f) => {
const Icon = navigationIcon[f.menuIcon] ?? FaDatabase
const isCustom = f.componentKind === WizardComponentKindEnum.Custom
return (
<div
key={f.fileName}
className="flex flex-col gap-2 p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800"
>
<div className="flex items-start gap-2 min-w-0">
<Icon className="text-indigo-400 shrink-0 text-2xl" />
<div className="min-w-0 flex-1 space-y-1">
{/* 1. satır: wizard adı */}
<div
className="font-medium text-sm text-gray-800 dark:text-gray-200 truncate"
title={f.wizardName || f.fileName}
>
{f.wizardName || f.fileName}
</div>
{/* 2. satır: dosya adı ve üretilen kod */}
<div className="text-xs text-gray-400 truncate font-mono" title={f.fileName}>
{f.fileName}
</div>
<div className="text-xs text-gray-400 truncate">
{isCustom ? f.customComponentName : f.listFormCode}
</div>
</div>
</div>
{/* 3. satır: yol ayrımı (Custom / List), layout ve menü adresi */}
<div className="flex flex-wrap items-center gap-1.5 text-xs">
<span
className={classNames(
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-medium',
isCustom
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300'
: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300',
)}
>
{isCustom ? <FaCube /> : <FaListUl />}
{translate(
isCustom
? '::App.WizardStepComponent.CustomComponent'
: '::App.WizardStepComponent.ListComponent',
)}
</span>
{/* Custom yolunda layout kavramı yoktur. */}
{!isCustom && f.defaultLayout && (
<span className="inline-flex items-center rounded-full bg-gray-200 px-2 py-0.5 font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-300">
{f.defaultLayout}
</span>
)}
{f.menuUrl && (
<span
className="inline-flex min-w-0 max-w-full items-center gap-1 rounded-full bg-gray-200 px-2 py-0.5 font-mono text-gray-600 dark:bg-gray-700 dark:text-gray-300"
title={f.menuUrl}
>
<FaLink className="shrink-0" />
<span className="truncate">{f.menuUrl}</span>
</span>
)}
</div>
{renderActions(
f,
'flex items-center justify-end gap-1 pt-1 border-t border-gray-200 dark:border-gray-700',
)}
</div>
)
})}
</div>
) : (
<div className="space-y-2">
{filteredFiles.map((f) => {
const Icon = navigationIcon[f.menuIcon] ?? FaDatabase
@ -307,77 +480,12 @@ const WizardFileManager = () => {
</div>
</div>
<div className="flex items-center gap-2 shrink-0 sm:ml-3">
{!f.hasInsertedRecords && (
<span
title={translate('::App.ListForm.WizardFileNoTrackedRecords')}
className="text-yellow-500 text-xs flex items-center gap-1"
>
<FaExclamationTriangle />
</span>
)}
{/* Menünün açtığı adres yeni sekmede açılır; adres yoksa buton anlamsızdır. */}
{f.menuUrl && (
<Button
size="sm"
variant="plain"
className="text-emerald-500 hover:bg-emerald-50 dark:hover:bg-emerald-900/20"
type="button"
title={translate('::App.Platform.OpenUrl')}
onClick={() => window.open(f.menuUrl, '_blank', 'noopener,noreferrer')}
>
<FaExternalLinkAlt />
</Button>
)}
{canExport && (
<Button
size="sm"
variant="plain"
className="text-sky-500 hover:bg-sky-50 dark:hover:bg-sky-900/20"
type="button"
title={translate('::App.ListForm.WizardFileExport')}
loading={exportingFile === f.fileName}
onClick={() => handleExport(f)}
>
<FaFileExport />
</Button>
)}
{canUpdate && (
<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')}
onClick={() =>
navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizard, {
state: { editFileName: f.fileName },
})
}
>
<FaEdit />
</Button>
)}
{canDelete && (
<Button
size="sm"
variant="plain"
className="text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
type="button"
title={translate('::App.Platform.Delete')}
loading={deletingFile === f.fileName}
onClick={() =>
setConfirm({ fileName: f.fileName, wizardName: f.wizardName || f.fileName })
}
>
<FaTrash />
</Button>
)}
</div>
{renderActions(f, 'flex items-center gap-1 shrink-0 sm:ml-3')}
</div>
)
})}
</div>
)}
</div>
{importFile && (

View file

@ -12,6 +12,8 @@ import {
FaExternalLinkAlt,
FaCog,
FaSyncAlt,
FaTh,
FaList,
} from 'react-icons/fa'
import Widget from '@/components/common/Widget'
import { ROUTES_ENUM } from '@/routes/route.constant'
@ -26,6 +28,12 @@ import { usePermission } from '@/utils/hooks/usePermission'
import { COMPONENT_PERMISSION } from '@/constants/permission.constant'
import Input from '@/components/ui/Input'
import Select from '@/components/ui/Select'
import type { CustomComponent } from '@/proxy/developerKit/models'
import { useStoreActions, useStoreState } from '@/store/store'
import type { ListViewLayoutType } from '../admin/listForm/edit/types'
/** Layout tercihi diger listelerle ayni yerde (admin.lists.states) saklanir. */
const VIEW_STATE_CODE = 'developerkit-components'
const ComponentManager: React.FC = () => {
const {
@ -40,6 +48,12 @@ const ComponentManager: React.FC = () => {
const [filterActive, setFilterActive] = useState<'all' | 'active' | 'inactive'>('all')
const [isRefreshing, setIsRefreshing] = useState(false)
const [editorComponentId, setEditorComponentId] = useState<string | null | undefined>(undefined)
const { states } = useStoreState((state) => state.admin.lists)
const { setStates } = useStoreActions((a) => a.admin.lists)
const viewMode: ListViewLayoutType =
states.find((s) => s.listFormCode === VIEW_STATE_CODE)?.layout === 'grid' ? 'grid' : 'card'
const setViewMode = (layout: ListViewLayoutType) =>
setStates({ listFormCode: VIEW_STATE_CODE, layout })
// Calculate statistics
const totalComponents = components?.length || 0
@ -104,6 +118,85 @@ const ComponentManager: React.FC = () => {
}
}
const renderStatusToggle = (component: CustomComponent) => (
<Button
type="button"
variant="plain"
shape="circle"
disabled={!canUpdate}
className="!inline-flex !h-auto items-center gap-1 rounded !px-2 py-1 text-xs font-medium transition-colors"
onClick={() => handleToggleActive(component.id, !component.isActive)}
>
{component.isActive ? (
<>
<FaEye className="w-4 h-4" />
{translate('::App.Listform.ListformField.IsActive')}
</>
) : (
<>
<FaEyeSlash className="w-4 h-4" />
{translate('::App.Platform.Passive')}
</>
)}
</Button>
)
const renderActions = (component: CustomComponent) => (
<div className="flex items-center gap-1">
{canUpdate && (
<Button
type="button"
variant="solid"
color="blue-600"
title={translate('::App.Platform.Edit')}
onClick={() =>
window.open(
ROUTES_ENUM.protected.saas.developerKit.componentsEdit.replace(':id', component.id),
'_blank',
)
}
>
<FaRegEdit className="w-4 h-4" />
</Button>
)}
<Button
type="button"
variant="solid"
color="gray-600"
disabled={!component.isActive || !component.routePath?.trim()}
title={translate('::App.Platform.OpenUrl')}
onClick={() => {
const routePath = component.routePath.startsWith('/')
? component.routePath
: `/${component.routePath}`
window.open(routePath, '_blank')
}}
>
<FaExternalLinkAlt className="w-4 h-4" />
</Button>
<Button
type="button"
variant="solid"
color="green-600"
title={translate('::App.Platform.View')}
onClick={() => setEditorComponentId(component.id)}
>
<FaCog className="w-4 h-4" />
</Button>
{canDelete && (
<Button
type="button"
variant="solid"
color="red-600"
title={translate('::App.Platform.Delete')}
onClick={() => handleDelete(component.id)}
>
<FaTrashAlt className="w-4 h-4" />
</Button>
)}
</div>
)
return (
<div className="space-y-4">
<PageTitle title={translate('::' + 'App.DeveloperKit.Components')} />
@ -181,6 +274,22 @@ const ComponentManager: React.FC = () => {
</Button>
</div>
)}
<div className="flex items-center gap-1">
<Button
type="button"
icon={<FaTh />}
variant={viewMode === 'card' ? 'solid' : 'default'}
title={translate('::App.Platform.CardView')}
onClick={() => setViewMode('card')}
/>
<Button
type="button"
icon={<FaList />}
variant={viewMode === 'grid' ? 'solid' : 'default'}
title={translate('::App.Platform.ListView')}
onClick={() => setViewMode('grid')}
/>
</div>
</div>
{/* Components List */}
@ -191,38 +300,45 @@ const ComponentManager: React.FC = () => {
</div>
</div>
) : filteredComponents?.length > 0 ? (
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6">
viewMode === 'card' ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-4 gap-4">
{filteredComponents.map((component) => (
<div
key={component.id}
className="bg-white dark:bg-gray-900 rounded-lg border border-slate-200 dark:border-gray-700 shadow-sm hover:shadow-md transition-shadow"
>
<div className="p-6">
<div className="p-4">
<div className="flex items-start justify-between">
{/* Sol taraf */}
<div className="flex-1">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="text-lg font-semibold text-slate-900 dark:text-gray-100">
<h3
className="text-base font-semibold text-slate-900 dark:text-gray-100 truncate"
title={component.name}
>
{component.name}
</h3>
<div
className={`w-2 h-2 rounded-full ${
className={`w-2 h-2 shrink-0 rounded-full ${
component.isActive ? 'bg-green-500' : 'bg-slate-300 dark:bg-gray-700'
}`}
/>
</div>
<p className="text-slate-600 dark:text-gray-300 text-sm mb-2">
<p className="text-slate-600 dark:text-gray-300 text-sm mb-2 truncate">
{parseComponentDependencies(component.dependencies).join(', ') ||
translate('::App.DeveloperKitComponent.NoDependencies')}
</p>
<p className="text-slate-600 dark:text-gray-300 text-sm mb-2">
<p
className="text-slate-600 dark:text-gray-300 text-sm mb-2 truncate"
title={component.routePath}
>
{component.routePath}
</p>
{component.description && (
<p className="text-slate-600 dark:text-gray-300 text-sm mb-2">
<p className="text-slate-600 dark:text-gray-300 text-sm mb-2 line-clamp-2">
{component.description}
</p>
)}
@ -237,9 +353,9 @@ const ComponentManager: React.FC = () => {
)}
</div>
{/* Sağ taraf */}
{/* Sag taraf */}
{component.lastModificationTime && (
<div className="flex items-center gap-1 text-xs text-slate-500 dark:text-gray-400 ml-4 whitespace-nowrap">
<div className="flex items-center gap-1 text-xs text-slate-500 dark:text-gray-400 ml-2 whitespace-nowrap">
<FaCalendarAlt className="w-3 h-3" />
<span>
{new Date(component.lastModificationTime).toLocaleDateString() ?? ''}
@ -249,90 +365,93 @@ const ComponentManager: React.FC = () => {
</div>
{/* Actions */}
<div className="flex items-center justify-between pt-2 border-t border-slate-100 dark:border-gray-700">
<div className="flex items-center gap-2">
<Button
type="button"
variant="plain"
shape="circle"
disabled={!canUpdate}
className={`!inline-flex !h-auto items-center gap-1 rounded !px-2 py-1 text-xs font-medium transition-colors`}
onClick={() => handleToggleActive(component.id, !component.isActive)}
>
{component.isActive ? (
<>
<FaEye className="w-4 h-4" />
{translate('::App.Listform.ListformField.IsActive')}
</>
) : (
<>
<FaEyeSlash className="w-4 h-4" />
{translate('::App.Platform.Passive')}
</>
)}
</Button>
</div>
<div className="flex items-center gap-1">
{canUpdate && (
<Button
type="button"
variant="solid"
color="blue-600"
title={translate('::App.Platform.Edit')}
onClick={() =>
window.open(
ROUTES_ENUM.protected.saas.developerKit.componentsEdit.replace(
':id',
component.id,
),
'_blank',
)
}
>
<FaRegEdit className="w-4 h-4" />
</Button>
)}
<Button
type="button"
variant="solid"
color="gray-600"
disabled={!component.isActive || !component.routePath?.trim()}
title={translate('::App.Platform.OpenUrl')}
onClick={() => {
const routePath = component.routePath.startsWith('/')
? component.routePath
: `/${component.routePath}`
window.open(routePath, '_blank')
}}
>
<FaExternalLinkAlt className="w-4 h-4" />
</Button>
<Button
type="button"
variant="solid"
color="green-600"
title={translate('::App.Platform.View')}
onClick={() => setEditorComponentId(component.id)}
>
<FaCog className="w-4 h-4" />
</Button>
{canDelete && (
<Button
type="button"
variant="solid"
color="red-600"
title={translate('::App.Platform.Delete')}
onClick={() => handleDelete(component.id)}
>
<FaTrashAlt className="w-4 h-4" />
</Button>
)}
</div>
<div className="flex items-center justify-between gap-1 pt-2 border-t border-slate-100 dark:border-gray-700">
{renderStatusToggle(component)}
{renderActions(component)}
</div>
</div>
</div>
))}
</div>
) : (
<div className="bg-white dark:bg-gray-900 rounded-lg border border-slate-200 dark:border-gray-700 shadow-sm overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-slate-50 dark:bg-gray-800 text-slate-600 dark:text-gray-300">
<tr>
<th className="text-left font-medium px-4 py-2">
{translate('::App.Platform.Name')}
</th>
<th className="text-left font-medium px-4 py-2">
{translate('::App.Platform.Dependencies')}
</th>
<th className="text-left font-medium px-4 py-2">
{translate('::App.Platform.RoutePath')}
</th>
<th className="text-left font-medium px-4 py-2">
{translate('::App.Platform.Description')}
</th>
<th className="text-left font-medium px-4 py-2 whitespace-nowrap">
{translate('::App.Platform.Modified')}
</th>
<th className="text-left font-medium px-4 py-2">
{translate('::App.Platform.Status')}
</th>
<th className="text-right font-medium px-4 py-2">
{translate('::App.Platform.Actions')}
</th>
</tr>
</thead>
<tbody>
{filteredComponents.map((component) => (
<tr
key={component.id}
className="border-t border-slate-100 dark:border-gray-700 hover:bg-slate-50 dark:hover:bg-gray-800"
>
<td className="px-4 py-2">
<div className="flex items-center gap-2">
<div
className={`w-2 h-2 shrink-0 rounded-full ${
component.isActive ? 'bg-green-500' : 'bg-slate-300 dark:bg-gray-700'
}`}
/>
<span className="font-medium text-slate-900 dark:text-gray-100">
{component.name}
</span>
</div>
{componentErrors[component.name] && (
<p
className="mt-1 text-xs text-red-700 dark:text-red-300 break-words"
title={componentErrors[component.name]}
>
{componentErrors[component.name]}
</p>
)}
</td>
<td className="px-4 py-2 text-slate-600 dark:text-gray-300">
{parseComponentDependencies(component.dependencies).join(', ') ||
translate('::App.DeveloperKitComponent.NoDependencies')}
</td>
<td className="px-4 py-2 text-slate-600 dark:text-gray-300">
{component.routePath}
</td>
<td className="px-4 py-2 text-slate-600 dark:text-gray-300">
{component.description}
</td>
<td className="px-4 py-2 text-slate-500 dark:text-gray-400 whitespace-nowrap">
{component.lastModificationTime
? new Date(component.lastModificationTime).toLocaleDateString()
: ''}
</td>
<td className="px-4 py-2">{renderStatusToggle(component)}</td>
<td className="px-4 py-2">
<div className="flex justify-end">{renderActions(component)}</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
) : (
<div className="text-center py-12">
<div className="mx-auto">

View file

@ -16,7 +16,7 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
import { Formik, Form, Field, FieldProps } from 'formik'
import * as Yup from 'yup'
import { FormContainer, FormItem } from '@/components/ui/Form'
import { Input, Checkbox, Button, Select } from '@/components/ui'
import { Input, Checkbox, Button, Select, Dialog } from '@/components/ui'
import { ConfirmDialog } from '@/components/shared'
import { CategoryIconOption, resolveCategoryIconOptions } from './categoryIcons'
@ -69,6 +69,11 @@ export function CategoryManagement({
setShowCreateForm(true)
}
const handleCloseForm = () => {
setShowCreateForm(false)
setEditingCategory(null)
}
const handleToggleActive = async (category: ForumCategory) => {
try {
await onUpdateCategoryActiveState(category.id)
@ -87,16 +92,24 @@ export function CategoryManagement({
return (
<div className="space-y-3">
{/* Create/Edit Form */}
{showCreateForm && (
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 className="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-100">
{/* Create/Edit Modal */}
<Dialog
isOpen={showCreateForm}
width={720}
onClose={handleCloseForm}
onRequestClose={handleCloseForm}
>
<div className="pb-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{editingCategory
? translate('::App.ForumCategoryManagement.EditCategory')
: translate('::App.ForumCategoryManagement.AddCategory')}
</h3>
</div>
<div className="pt-4 max-h-[70vh] overflow-y-auto">
<Formik
enableReinitialize
initialValues={{
name: editingCategory?.name ?? '',
slug: editingCategory?.slug ?? '',
@ -221,14 +234,7 @@ export function CategoryManagement({
</div>
<div className="mt-4 flex justify-end space-x-3">
<Button
variant="plain"
type="button"
onClick={() => {
setShowCreateForm(false)
setEditingCategory(null)
}}
>
<Button variant="plain" type="button" onClick={handleCloseForm}>
{translate('::App.Platform.Cancel')}
</Button>
<Button variant="solid" type="submit" loading={isSubmitting}>
@ -242,7 +248,7 @@ export function CategoryManagement({
)}
</Formik>
</div>
)}
</Dialog>
{/* Categories List */}
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
@ -321,7 +327,7 @@ export function CategoryManagement({
<div className="flex items-center space-x-2">
<Button
size="xs"
size="sm"
className={`p-1 rounded-lg transition-colors ${
category.isActive
? 'text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-900'
@ -335,14 +341,14 @@ export function CategoryManagement({
onClick={() => handleToggleActive(category)}
>
{category.isActive ? (
<FaEye className="w-3 h-3" />
<FaEye className="w-4 h-4" />
) : (
<FaEyeSlash className="w-3 h-3" />
<FaEyeSlash className="w-4 h-4" />
)}
</Button>
<Button
size="xs"
size="sm"
className={`p-1 rounded-lg transition-colors ${
category.isLocked
? 'text-yellow-600 dark:text-yellow-300 hover:bg-yellow-100 dark:hover:bg-yellow-900'
@ -356,28 +362,28 @@ export function CategoryManagement({
onClick={() => handleToggleLocked(category)}
>
{category.isLocked ? (
<FaLock className="w-3 h-3" />
<FaLock className="w-4 h-4" />
) : (
<FaUnlock className="w-3 h-3" />
<FaUnlock className="w-4 h-4" />
)}
</Button>
<Button
size="xs"
size="sm"
className="p-1 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900 rounded-lg transition-colors"
title={translate('::App.ForumCategoryManagement.EditCategory')}
onClick={() => handleEdit(category)}
>
<FaEdit className="w-3 h-3" />
<FaEdit className="w-4 h-4" />
</Button>
<Button
size="xs"
size="sm"
className="p-1 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900 rounded-lg transition-colors"
title={translate('::App.ForumCategoryManagement.DeleteCategory')}
onClick={() => confirmDeleteCategory(category)}
>
<FaTrash className="w-3 h-3" />
<FaTrash className="w-4 h-4" />
</Button>
</div>
</div>

View file

@ -15,7 +15,7 @@ import { useStoreState } from '@/store/store'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Formik, Form, Field, FieldProps } from 'formik'
import * as Yup from 'yup'
import { FormContainer, FormItem, Button } from '@/components/ui'
import { FormContainer, FormItem, Button, Dialog } from '@/components/ui'
import { ConfirmDialog } from '@/components/shared'
import { formatForumDate, sanitizeForumHtml, stripHtml } from '../forum/utils'
import {
@ -72,6 +72,11 @@ export function PostManagement({
setShowCreateForm(true)
}
const handleCloseForm = () => {
setShowCreateForm(false)
setEditingPost(null)
}
const handleToggleAcceptedAnswer = async (post: ForumPost) => {
try {
if (post.isAcceptedAnswer) {
@ -89,15 +94,22 @@ export function PostManagement({
return (
<div className="space-y-3">
{/* Create/Edit Form */}
{showCreateForm && (
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 className="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-100">
{/* Create/Edit Modal */}
<Dialog
isOpen={showCreateForm}
width={900}
onClose={handleCloseForm}
onRequestClose={handleCloseForm}
>
<div className="pb-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{editingPost
? translate('::App.ForumPostManagement.EditPost')
: translate('::App.ForumPostManagement.AddPost')}
</h3>
</div>
<div className="pt-4 max-h-[70vh] overflow-y-auto">
<Formik
enableReinitialize
initialValues={{
@ -234,14 +246,7 @@ export function PostManagement({
</FormItem>
<div className="flex justify-end space-x-3 pt-2">
<Button
variant="plain"
type="button"
onClick={() => {
setShowCreateForm(false)
setEditingPost(null)
}}
>
<Button variant="plain" type="button" onClick={handleCloseForm}>
{translate('::App.Platform.Cancel')}
</Button>
<Button
@ -260,7 +265,7 @@ export function PostManagement({
)}
</Formik>
</div>
)}
</Dialog>
{/* Posts List */}
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
@ -304,7 +309,7 @@ export function PostManagement({
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">{post.authorName}</h4>
{post.isAcceptedAnswer && (
<div className="flex items-center space-x-1 bg-emerald-100 dark:bg-emerald-900 text-emerald-700 dark:text-emerald-300 px-2 py-1 rounded-full text-xs">
<FaCheckCircle className="w-3 h-3" />
<FaCheckCircle className="w-4 h-4" />
<span>{translate('::App.ForumPostManagement.AcceptedAnswer')}</span>
</div>
)}
@ -336,7 +341,7 @@ export function PostManagement({
<div className="flex items-center space-x-2 ml-4">
<Button
size="xs"
size="sm"
className={`p-1 rounded-lg transition-colors ${
post.isAcceptedAnswer
? 'text-emerald-600 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900'
@ -350,28 +355,28 @@ export function PostManagement({
onClick={() => handleToggleAcceptedAnswer(post)}
>
{post.isAcceptedAnswer ? (
<FaCheckCircle className="w-3 h-3" />
<FaCheckCircle className="w-4 h-4" />
) : (
<FaCircle className="w-3 h-3" />
<FaCircle className="w-4 h-4" />
)}
</Button>
<Button
size="xs"
size="sm"
className="p-1 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900 rounded-lg transition-colors"
title={translate('::App.ForumPostManagement.EditPost')}
onClick={() => handleEdit(post)}
>
<FaEdit className="w-3 h-3" />
<FaEdit className="w-4 h-4" />
</Button>
<Button
size="xs"
size="sm"
className="p-1 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900 rounded-lg transition-colors"
title={translate('::App.ForumPostManagement.DeletePost')}
onClick={() => confirmDeletePost(post)}
>
<FaTrashAlt className="w-3 h-3" />
<FaTrashAlt className="w-4 h-4" />
</Button>
</div>
</div>

View file

@ -17,7 +17,7 @@ import { useStoreState } from '@/store/store'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Formik, Form, Field, FieldProps } from 'formik'
import * as Yup from 'yup'
import { FormContainer, FormItem, Button, Select } from '@/components/ui'
import { FormContainer, FormItem, Button, Select, Dialog } from '@/components/ui'
import { ConfirmDialog } from '@/components/shared'
import { formatForumDate } from '../forum/utils'
@ -84,6 +84,11 @@ export function TopicManagement({
setShowCreateForm(true)
}
const handleCloseForm = () => {
setShowCreateForm(false)
setEditingTopic(null)
}
const handlePin = async (topic: ForumTopic) => {
try {
if (topic.isPinned) {
@ -141,15 +146,22 @@ export function TopicManagement({
return (
<div className="space-y-3">
{/* Create/Edit Form */}
{showCreateForm && (
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 className="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-100">
{/* Create/Edit Modal */}
<Dialog
isOpen={showCreateForm}
width={720}
onClose={handleCloseForm}
onRequestClose={handleCloseForm}
>
<div className="pb-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{editingTopic
? translate('::App.ForumTopicManagement.EditTopic')
: translate('::App.ForumTopicManagement.AddTopic')}
</h3>
</div>
<div className="pt-4 max-h-[70vh] overflow-y-auto">
<Formik
enableReinitialize
initialValues={
@ -260,14 +272,7 @@ export function TopicManagement({
</FormItem>
<div className="flex justify-end space-x-3 pt-2">
<Button
variant="plain"
type="button"
onClick={() => {
setShowCreateForm(false)
setEditingTopic(null)
}}
>
<Button variant="plain" type="button" onClick={handleCloseForm}>
{translate('::App.Platform.Cancel')}
</Button>
<Button variant="solid" type="submit" loading={isSubmitting}>
@ -279,7 +284,7 @@ export function TopicManagement({
)}
</Formik>
</div>
)}
</Dialog>
{/* Topics List */}
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
@ -366,7 +371,7 @@ export function TopicManagement({
<div className="flex items-center space-x-2 ml-4">
<Button
size="xs"
size="sm"
className={`p-1 rounded-lg transition-colors ${
topic.isPinned
? 'text-orange-600 dark:text-orange-400 hover:bg-orange-100 dark:hover:bg-orange-900'
@ -379,11 +384,11 @@ export function TopicManagement({
}
onClick={() => handlePin(topic)}
>
<FaThumbtack className="h-3 w-3" />
<FaThumbtack className="h-4 w-4" />
</Button>
<Button
size="xs"
size="sm"
className={`p-1 rounded-lg transition-colors ${
topic.isLocked
? 'text-yellow-600 dark:text-yellow-300 hover:bg-yellow-100 dark:hover:bg-yellow-900'
@ -397,14 +402,14 @@ export function TopicManagement({
onClick={() => handleLock(topic)}
>
{topic.isLocked ? (
<FaLock className="w-3 h-3" />
<FaLock className="w-4 h-4" />
) : (
<FaUnlock className="w-3 h-3" />
<FaUnlock className="w-4 h-4" />
)}
</Button>
<Button
size="xs"
size="sm"
className={`p-1 rounded-lg transition-colors ${
topic.isSolved
? 'text-emerald-600 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900'
@ -418,28 +423,28 @@ export function TopicManagement({
onClick={() => handleSolved(topic)}
>
{topic.isSolved ? (
<FaCheckCircle className="w-3 h-3" />
<FaCheckCircle className="w-4 h-4" />
) : (
<FaCircle className="w-3 h-3" />
<FaCircle className="w-4 h-4" />
)}
</Button>
<Button
size="xs"
size="sm"
className="p-1 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900 rounded-lg transition-colors"
title={translate('::App.ForumTopicManagement.EditTopic')}
onClick={() => handleEdit(topic)}
>
<FaEdit className="w-3 h-3" />
<FaEdit className="w-4 h-4" />
</Button>
<Button
size="xs"
size="sm"
className="p-1 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900 rounded-lg transition-colors"
title={translate('::App.ForumTopicManagement.DeleteTopic')}
onClick={() => confirmDeleteTopic(topic)}
>
<FaTrashAlt className="w-3 h-3" />
<FaTrashAlt className="w-4 h-4" />
</Button>
</div>
</div>