git feature

This commit is contained in:
2026-06-30 14:46:19 +07:00
parent 251c1b7573
commit 11e5a76977
23 changed files with 1680 additions and 28 deletions

View File

@@ -7,8 +7,9 @@ import {
type ExamSubmission,
type StudentItem,
} from '../api';
import { useGitHubConnection } from '../hooks/useGitHubConnection';
import { navigate, pushNav } from '../navigation';
import { BASE_APP_SUGGESTIONS, DEFAULT_EXAM_ALLOWED_APPS } from '../constants';
import { pushNav } from '../navigation';
import { openStaffChat } from '../chatEvents';
import { AppPoolModal } from './AppPoolModal';
import { NavHistoryBar } from './NavHistoryBar';
@@ -49,6 +50,10 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
const [end, setEnd] = useState('');
const [allowedApps, setAllowedApps] = useState(DEFAULT_EXAM_ALLOWED_APPS);
const [quizUrl, setQuizUrl] = useState('');
const [gitPublishUrl, setGitPublishUrl] = useState('');
const [bundlingSubs, setBundlingSubs] = useState(false);
const [publishingGit, setPublishingGit] = useState(false);
const gh = useGitHubConnection();
const [editable, setEditable] = useState(false);
const [prepEditable, setPrepEditable] = useState(false);
const [displayStatus, setDisplayStatus] = useState('draft');
@@ -87,6 +92,7 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
setEnd(toLocalInput(data.room.endTime));
setAllowedApps(data.room.allowedApps || DEFAULT_EXAM_ALLOWED_APPS);
setQuizUrl(data.room.quizUrl || '');
setGitPublishUrl(data.room.gitPublishUrl || '');
setEditable(data.editable);
setPrepEditable(data.prepEditable ?? data.editable);
setDisplayStatus(data.displayStatus);
@@ -230,6 +236,39 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
URL.revokeObjectURL(url);
};
const downloadAllSubs = async () => {
setErr(''); setMsg('');
setBundlingSubs(true);
try {
await apiExam.downloadAllSubmissions(examId, roomName || `phong_thi_${examId}`);
setMsg('Đã tải ZIP gộp — giải nén sẽ thấy từng folder bài làm theo sinh viên.');
} catch (e: any) {
setErr(e?.message || 'Tải thất bại');
} finally {
setBundlingSubs(false);
}
};
const publishToGit = async () => {
if (!gh.connected) {
setErr('Kết nối GitHub trong Tài khoản của tôi (sidebar dưới cùng) trước khi đẩy.');
return;
}
setErr(''); setMsg('');
setPublishingGit(true);
try {
const res = await apiExam.publishSubmissionsGit(examId);
const openUrl = res.openUrl || res.gitPublishUrl || res.url;
setGitPublishUrl(openUrl);
setMsg(res.message || 'Đã đẩy bài nộp lên GitHub');
if (openUrl) window.open(openUrl, '_blank', 'noopener,noreferrer');
} catch (e: any) {
setErr(e?.message || 'Đẩy Git thất bại');
} finally {
setPublishingGit(false);
}
};
const handlePublish = async () => {
setErr(''); setMsg('');
try {
@@ -408,6 +447,31 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
<span>Link trắc nghiệm</span>
<input value={quizUrl} onChange={(e) => setQuizUrl(e.target.value)} placeholder="https://..." disabled={!editable} />
</label>
<div className="exam-git-settings">
<p className="exam-git-status-line">
GitHub:{' '}
{gh.connected ? (
<strong>@{gh.githubLogin}</strong>
) : (
<>
<span className="exam-git-not-connected">chưa kết nối</span>
{' — '}
<button type="button" className="link-btn" onClick={() => navigate('profile')}>
Kết nối trong Tài khoản
</button>
</>
)}
</p>
<p className="exam-git-hint">
Bấm <strong>Đy lên Git</strong> sẽ tự tạo repo theo tên phòng thi mỗi sinh viên một folder, giống ZIP tải về.
</p>
{gitPublishUrl && (
<p className="exam-git-last">
Repo gần nhất:{' '}
<a href={gitPublishUrl} target="_blank" rel="noreferrer">{gitPublishUrl}</a>
</p>
)}
</div>
<label className="login-field">
<span>Bắt đu</span>
<input type="datetime-local" value={start} onChange={(e) => setStart(e.target.value)} disabled={!editable} />
@@ -766,6 +830,38 @@ export const ExamRoomWorkspace: React.FC<Props> = ({ examId, onBack }) => {
</div>
) : (
<div className="session-logs-panel">
{submissions.length > 0 && (
<div className="exam-submissions-toolbar">
<button
type="button"
className="btn btn-primary btn-sm"
disabled={bundlingSubs}
onClick={downloadAllSubs}
>
{bundlingSubs ? 'Đang gói ZIP...' : '📦 Tải tất cả (ZIP gộp)'}
</button>
<button
type="button"
className="btn btn-secondary btn-sm"
disabled={publishingGit || !gh.connected}
onClick={publishToGit}
title={gh.connected ? 'Tạo repo + đẩy từng folder sinh viên lên GitHub' : 'Kết nối GitHub trong Tài khoản trước'}
>
{publishingGit ? 'Đang đẩy Git...' : '🔗 Đẩy lên Git'}
</button>
{!gh.connected && (
<span className="exam-submissions-toolbar-hint">
<button type="button" className="link-btn" onClick={() => navigate('profile')}>
Kết nối GitHub
</button>
{' '}trong Tài khoản của tôi trước khi đy.
</span>
)}
<span className="exam-submissions-toolbar-hint">
ZIP gộp: mỗi sinh viên một folder giải nén chấm ngay.
</span>
</div>
)}
<div className="attendance-table-scroll table-wrapper" style={{ border: 'none' }}>
{submissions.length === 0 ? (
<div className="empty-state" style={{ minHeight: '300px' }}>