Wizard ve Component Manager List ve Card görünümleri
This commit is contained in:
parent
4c09de9ad0
commit
90a66562e7
6 changed files with 610 additions and 325 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,138 +343,149 @@ const WizardFileManager = () => {
|
|||
</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{filteredFiles.map((f) => {
|
||||
const Icon = navigationIcon[f.menuIcon] ?? FaDatabase
|
||||
const isCustom = f.componentKind === WizardComponentKindEnum.Custom
|
||||
return (
|
||||
<div
|
||||
key={f.fileName}
|
||||
className="flex flex-col sm:flex-row sm:items-center sm:justify-between 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-center gap-3 min-w-0">
|
||||
<Icon className="text-indigo-400 shrink-0 text-3xl" />
|
||||
<div className="min-w-0 space-y-1">
|
||||
{/* 1. satır: wizard adı */}
|
||||
<div className="font-medium text-sm text-gray-800 dark:text-gray-200 truncate">
|
||||
{f.wizardName || f.fileName}
|
||||
</div>
|
||||
|
||||
{/* 2. satır: dosya adı ve üretilen kod */}
|
||||
<div className="flex flex-wrap items-center gap-x-2 text-xs text-gray-400">
|
||||
<span className="truncate font-mono">{f.fileName}</span>
|
||||
<span className="text-gray-300 dark:text-gray-600">•</span>
|
||||
<span className="truncate">
|
||||
{isCustom ? f.customComponentName : f.listFormCode}
|
||||
</span>
|
||||
</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',
|
||||
)}
|
||||
{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}
|
||||
>
|
||||
{isCustom ? <FaCube /> : <FaListUl />}
|
||||
{translate(
|
||||
isCustom
|
||||
? '::App.WizardStepComponent.CustomComponent'
|
||||
: '::App.WizardStepComponent.ListComponent',
|
||||
)}
|
||||
</span>
|
||||
{f.wizardName || f.fileName}
|
||||
</div>
|
||||
|
||||
{/* 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 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">
|
||||
<FaLink className="shrink-0" />
|
||||
<span className="truncate">{f.menuUrl}</span>
|
||||
</span>
|
||||
)}
|
||||
{/* 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>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0 sm:ml-3">
|
||||
{!f.hasInsertedRecords && (
|
||||
{/* 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
|
||||
title={translate('::App.ListForm.WizardFileNoTrackedRecords')}
|
||||
className="text-yellow-500 text-xs flex items-center gap-1"
|
||||
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',
|
||||
)}
|
||||
>
|
||||
<FaExclamationTriangle />
|
||||
{isCustom ? <FaCube /> : <FaListUl />}
|
||||
{translate(
|
||||
isCustom
|
||||
? '::App.WizardStepComponent.CustomComponent'
|
||||
: '::App.WizardStepComponent.ListComponent',
|
||||
)}
|
||||
</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>
|
||||
|
||||
{/* 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>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{filteredFiles.map((f) => {
|
||||
const Icon = navigationIcon[f.menuIcon] ?? FaDatabase
|
||||
const isCustom = f.componentKind === WizardComponentKindEnum.Custom
|
||||
return (
|
||||
<div
|
||||
key={f.fileName}
|
||||
className="flex flex-col sm:flex-row sm:items-center sm:justify-between 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-center gap-3 min-w-0">
|
||||
<Icon className="text-indigo-400 shrink-0 text-3xl" />
|
||||
<div className="min-w-0 space-y-1">
|
||||
{/* 1. satır: wizard adı */}
|
||||
<div className="font-medium text-sm text-gray-800 dark:text-gray-200 truncate">
|
||||
{f.wizardName || f.fileName}
|
||||
</div>
|
||||
|
||||
{/* 2. satır: dosya adı ve üretilen kod */}
|
||||
<div className="flex flex-wrap items-center gap-x-2 text-xs text-gray-400">
|
||||
<span className="truncate font-mono">{f.fileName}</span>
|
||||
<span className="text-gray-300 dark:text-gray-600">•</span>
|
||||
<span className="truncate">
|
||||
{isCustom ? f.customComponentName : f.listFormCode}
|
||||
</span>
|
||||
</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 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">
|
||||
<FaLink className="shrink-0" />
|
||||
<span className="truncate">{f.menuUrl}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{renderActions(f, 'flex items-center gap-1 shrink-0 sm:ml-3')}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{importFile && (
|
||||
|
|
|
|||
|
|
@ -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,148 +300,158 @@ 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">
|
||||
{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="flex items-start justify-between">
|
||||
{/* Sol taraf */}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-gray-100">
|
||||
{component.name}
|
||||
</h3>
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
component.isActive ? 'bg-green-500' : 'bg-slate-300 dark:bg-gray-700'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
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-4">
|
||||
<div className="flex items-start justify-between">
|
||||
{/* Sol taraf */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<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 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">
|
||||
{parseComponentDependencies(component.dependencies).join(', ') ||
|
||||
translate('::App.DeveloperKitComponent.NoDependencies')}
|
||||
</p>
|
||||
|
||||
<p className="text-slate-600 dark:text-gray-300 text-sm mb-2">
|
||||
{component.routePath}
|
||||
</p>
|
||||
|
||||
{component.description && (
|
||||
<p className="text-slate-600 dark:text-gray-300 text-sm mb-2">
|
||||
{component.description}
|
||||
<p className="text-slate-600 dark:text-gray-300 text-sm mb-2 truncate">
|
||||
{parseComponentDependencies(component.dependencies).join(', ') ||
|
||||
translate('::App.DeveloperKitComponent.NoDependencies')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{componentErrors[component.name] && (
|
||||
<p
|
||||
className="text-xs text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded px-2 py-1 mb-2 break-words"
|
||||
title={componentErrors[component.name]}
|
||||
className="text-slate-600 dark:text-gray-300 text-sm mb-2 truncate"
|
||||
title={component.routePath}
|
||||
>
|
||||
{componentErrors[component.name]}
|
||||
{component.routePath}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sağ taraf */}
|
||||
{component.lastModificationTime && (
|
||||
<div className="flex items-center gap-1 text-xs text-slate-500 dark:text-gray-400 ml-4 whitespace-nowrap">
|
||||
<FaCalendarAlt className="w-3 h-3" />
|
||||
<span>
|
||||
{new Date(component.lastModificationTime).toLocaleDateString() ?? ''}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</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')}
|
||||
</>
|
||||
{component.description && (
|
||||
<p className="text-slate-600 dark:text-gray-300 text-sm mb-2 line-clamp-2">
|
||||
{component.description}
|
||||
</p>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{componentErrors[component.name] && (
|
||||
<p
|
||||
className="text-xs text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded px-2 py-1 mb-2 break-words"
|
||||
title={componentErrors[component.name]}
|
||||
>
|
||||
{componentErrors[component.name]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sag taraf */}
|
||||
{component.lastModificationTime && (
|
||||
<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() ?? ''}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<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>
|
||||
))}
|
||||
</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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue