diff --git a/ui/src/main.js b/ui/src/main.js index 0b38856..873e6f1 100644 --- a/ui/src/main.js +++ b/ui/src/main.js @@ -6,6 +6,7 @@ GnButton, GnDescriptionList, GnEmptyState, + GnInput, GnMetricCard, GnPageHeader, GnTabs, @@ -41,6 +42,7 @@ GnButton, GnDescriptionList, GnEmptyState, + GnInput, GnMetricCard, GnPageHeader, GnTabs, @@ -71,6 +73,12 @@ const changeApplying = ref(false); const changeError = ref(""); const gitStatus = ref(null); + const selectedCommitFiles = ref([]); + const commitSummary = ref(""); + const commitDetails = ref(""); + const commitRunning = ref(false); + const commitError = ref(""); + const commitResult = ref(null); const tabs = [ { id: "docs", label: "Docs", icon: "ph-files" }, @@ -147,6 +155,52 @@ return "secondary"; }; + const isCommittablePath = (path) => { + if (!path) { + return false; + } + const denied = [ + ".codex", + ".git", + ".pytest_cache", + ".ruff_cache", + ".venv", + "__pycache__", + "gnexus_book_server.egg-info", + "node_modules" + ]; + return !path.split("/").some((part) => denied.includes(part)); + }; + + const committableEntries = computed(() => + (gitStatus.value?.entries || []).filter((entry) => isCommittablePath(entry.path)) + ); + + const commitCanRun = computed( + () => commitSummary.value.trim().length > 0 && selectedCommitFiles.value.length > 0 && !commitRunning.value + ); + + const syncCommitSelection = (gitData) => { + const allowed = (gitData?.entries || []) + .map((entry) => entry.path) + .filter((path) => isCommittablePath(path)); + selectedCommitFiles.value = selectedCommitFiles.value.filter((path) => allowed.includes(path)); + if (!selectedCommitFiles.value.length) { + selectedCommitFiles.value = allowed; + } + }; + + const toggleCommitFile = (path) => { + if (!isCommittablePath(path)) { + return; + } + if (selectedCommitFiles.value.includes(path)) { + selectedCommitFiles.value = selectedCommitFiles.value.filter((item) => item !== path); + } else { + selectedCommitFiles.value = [...selectedCommitFiles.value, path]; + } + }; + const loadInventoryType = async (type) => { selectedInventoryType.value = type; selectedInventoryId.value = ""; @@ -201,6 +255,7 @@ validation.value = validationData; freshness.value = freshnessData; gitStatus.value = gitData; + syncCommitSelection(gitData); } catch (caught) { changeError.value = caught instanceof Error ? caught.message : "Failed to apply change"; } finally { @@ -208,6 +263,35 @@ } }; + const commitSelectedFiles = async () => { + if (!commitCanRun.value) { + return; + } + commitRunning.value = true; + commitError.value = ""; + commitResult.value = null; + try { + commitResult.value = await apiPost("/commit", { + summary: commitSummary.value.trim(), + details: commitDetails.value.trim(), + files: selectedCommitFiles.value + }); + commitSummary.value = ""; + commitDetails.value = ""; + const [gitData, validationData] = await Promise.all([ + apiGet("/git/status"), + apiGet("/validate") + ]); + gitStatus.value = gitData; + validation.value = validationData; + syncCommitSelection(gitData); + } catch (caught) { + commitError.value = caught instanceof Error ? caught.message : "Commit failed"; + } finally { + commitRunning.value = false; + } + }; + const load = async () => { loading.value = true; error.value = ""; @@ -236,6 +320,7 @@ inventoryTypes.value = inventoryData; changes.value = changesData; gitStatus.value = gitData; + syncCommitSelection(gitData); if (!selectedChangeId.value && changesData.length) { await loadChange(changesData[0].id); } @@ -262,6 +347,14 @@ changeLoading, changeStatusVariant, changes, + commitCanRun, + commitDetails, + commitError, + commitResult, + commitRunning, + commitSelectedFiles, + commitSummary, + committableEntries, docError, docLoading, docs, @@ -289,8 +382,11 @@ selectedChange, selectedChangeId, selectedChangeJson, + selectedCommitFiles, selectInventoryRecord, loadChange, + toggleCommitFile, + isCommittablePath, tabs, validation, validationVariant @@ -457,11 +553,46 @@ diff --git a/ui/src/styles.css b/ui/src/styles.css index 99d9e82..73a98af 100644 --- a/ui/src/styles.css +++ b/ui/src/styles.css @@ -53,7 +53,8 @@ .gnb-inventory, .gnb-docs, -.gnb-changes { +.gnb-changes, +.gnb-git { display: grid; grid-template-columns: minmax(220px, 320px) minmax(0, 1fr); gap: 24px; @@ -113,6 +114,46 @@ white-space: pre-wrap; } +.gnb-checkbox-row { + grid-template-columns: 24px 48px minmax(0, 1fr); +} + +.gnb-checkbox-row input { + width: 16px; + height: 16px; +} + +.gnb-checkbox-row.is-disabled { + opacity: 0.52; +} + +.gnb-commit { + display: grid; + gap: 16px; + min-width: 0; +} + +.gnb-field { + display: grid; + gap: 6px; +} + +.gnb-field span { + font-size: 13px; + font-weight: 600; +} + +.gnb-field textarea { + width: 100%; + box-sizing: border-box; + resize: vertical; + padding: 10px 12px; + border: 1px solid var(--gn-border, #dfe3e8); + background: var(--gn-surface, #ffffff); + color: var(--gn-text, #101828); + font: inherit; +} + .gnb-error { margin-top: 16px; color: var(--gn-danger, #b42318); @@ -127,6 +168,7 @@ .gnb-row, .gnb-docs, .gnb-changes, + .gnb-git, .gnb-inventory, .gnb-detail-grid { grid-template-columns: 1fr;