clean ui
This commit is contained in:
131
management/src/components/AppTemplatePickerModal.tsx
Normal file
131
management/src/components/AppTemplatePickerModal.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { apiAppTemplates, type AppTemplateItem } from '../api';
|
||||
import { countKeywords } from '../utils/appKeywords';
|
||||
|
||||
interface AppTemplatePickerModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSelect: (keywords: string) => void;
|
||||
allowedApps: string;
|
||||
}
|
||||
|
||||
export const AppTemplatePickerModal: React.FC<AppTemplatePickerModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSelect,
|
||||
allowedApps,
|
||||
}) => {
|
||||
const [items, setItems] = useState<AppTemplateItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await apiAppTemplates.list();
|
||||
setItems(res.data || []);
|
||||
} catch (e: any) {
|
||||
setItems([]);
|
||||
setError(e?.message || 'Không tải được khung ứng dụng');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
load();
|
||||
}, [open, load]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setSearch('');
|
||||
setError(null);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const q = search.trim().toLowerCase();
|
||||
const filtered = q
|
||||
? items.filter(
|
||||
(t) =>
|
||||
t.name.toLowerCase().includes(q) ||
|
||||
t.description?.toLowerCase().includes(q) ||
|
||||
t.keywords.toLowerCase().includes(q),
|
||||
)
|
||||
: items;
|
||||
|
||||
return (
|
||||
<div className="modal-overlay app-pool-overlay" onClick={onClose}>
|
||||
<div className="modal-container app-pool-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<div>
|
||||
<h2 className="modal-title" style={{ margin: 0 }}>Chọn khung ứng dụng</h2>
|
||||
<p className="app-pool-modal-sub">
|
||||
Ghép bộ keyword đã lưu vào danh sách ứng dụng được phép. Quản lý khung tại Hệ thống → Khung ứng dụng.
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" className="btn btn-secondary" onClick={onClose}>
|
||||
Đóng
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="app-pool-toolbar">
|
||||
<input
|
||||
type="search"
|
||||
className="app-pool-search"
|
||||
placeholder="Tìm theo tên, mô tả, keyword..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<button type="button" className="btn btn-secondary" onClick={load} disabled={loading}>
|
||||
{loading ? 'Đang tải...' : 'Làm mới'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="app-pool-body">
|
||||
{error ? (
|
||||
<div className="discovered-apps-empty discovered-apps-error">{error}</div>
|
||||
) : loading && items.length === 0 ? (
|
||||
<div className="app-pool-status">Đang tải...</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="app-pool-status">
|
||||
{q ? `Không tìm thấy "${search}"` : 'Chưa có khung nào. Tạo tại Hệ thống → Khung ứng dụng.'}
|
||||
</div>
|
||||
) : (
|
||||
<div className="template-picker-list">
|
||||
{filtered.map((tpl) => (
|
||||
<div key={tpl.id} className="template-picker-card">
|
||||
<div className="template-picker-card-head">
|
||||
<strong>{tpl.name}</strong>
|
||||
<span className="template-picker-count">{countKeywords(tpl.keywords)} keyword</span>
|
||||
</div>
|
||||
{tpl.description && <p className="template-picker-desc">{tpl.description}</p>}
|
||||
<code className="template-picker-kw">{tpl.keywords}</code>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-sm template-picker-apply"
|
||||
onClick={() => {
|
||||
onSelect(tpl.keywords);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Áp dụng khung
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="app-pool-footer">
|
||||
Đang có {countKeywords(allowedApps)} keyword trong whitelist hiện tại
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user