132 lines
4.3 KiB
TypeScript
132 lines
4.3 KiB
TypeScript
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>
|
|
);
|
|
};
|