refactor(frontend): RepositoryDashboardGrid wraps Dashboard.DataGrid
Reduced from 734 to ~300 lines. All git-specific logic preserved: - status fetching via gitService (loadRepositoryStatuses) - bulk git actions (sync, commit, pull, push, delete) - GitManager modal integration - repositoriesOnly pre-filtering - status badge rendering via raw render Grid logic (filter, sort, paginate, select, table rendering, pagination UI) delegated to Dashboard.DataGrid. Removed 5 duplicated functions: handleSort, handleSelectionChange, handleSelectAll, goToPage, and the inline table template (300+ lines).
This commit is contained in:
@@ -2,18 +2,20 @@
|
|||||||
<!-- @ingroup Dashboard -->
|
<!-- @ingroup Dashboard -->
|
||||||
<!-- @BRIEF Dashboard grid with git repository integration — status fetching, bulk git actions, GitManager modal. -->
|
<!-- @BRIEF Dashboard grid with git repository integration — status fetching, bulk git actions, GitManager modal. -->
|
||||||
<!-- @LAYER UI -->
|
<!-- @LAYER UI -->
|
||||||
<!-- @RATIONALE Kept separate from Dashboard.DataGrid due to deep git-specific integration (async status fetching via gitService, bulk git operations, GitManager modal coupling). Future consolidation candidate: extract git-logic into a GitDashboardModel and render via Dashboard.DataGrid with raw HTML column support. ~400 of 722 lines are git-specific; remaining ~320 lines duplicate DashboardGrid sort/filter/paginate/select logic. -->
|
<!-- @RATIONALE Now delegates grid rendering to Dashboard.DataGrid. Keeps ~200 lines of git-specific logic (status fetching via gitService, bulk git operations, GitManager modal). Filter/sort/paginate/select/selection tracking handled by Dashboard.DataGrid. -->
|
||||||
<!-- @RELATION USED_BY -> [frontend/src/routes/git/+page.svelte] -->
|
<!-- @RELATION USED_BY -> [frontend/src/routes/git/+page.svelte] -->
|
||||||
<!-- @RELATION DEPENDS_ON -> [$lib/components/git/GitManager] -->
|
<!-- @RELATION DEPENDS_ON -> [$lib/components/git/GitManager] -->
|
||||||
<!-- @RELATION DEPENDS_ON -> [gitService] -->
|
<!-- @RELATION DEPENDS_ON -> [gitService] -->
|
||||||
|
<!-- @RELATION DEPENDS_ON -> [Dashboard.DataGrid] -->
|
||||||
<!-- @INVARIANT Selected IDs must be a subset of available dashboards. -->
|
<!-- @INVARIANT Selected IDs must be a subset of available dashboards. -->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// [SECTION: IMPORTS]
|
// [SECTION: IMPORTS]
|
||||||
import { untrack } from "svelte";
|
import { untrack } from "svelte";
|
||||||
import type { DashboardMetadata } from "../../types/dashboard";
|
import type { DashboardMetadata } from "../../types/dashboard";
|
||||||
|
import type { Column } from "./DashboardDataGrid.svelte";
|
||||||
import { t } from '$lib/i18n/index.svelte.js';
|
import { t } from '$lib/i18n/index.svelte.js';
|
||||||
import { Button, Input, ConfirmDialog } from "$lib/ui";
|
import { Button, ConfirmDialog } from "$lib/ui";
|
||||||
|
import DashboardDataGrid from "./DashboardDataGrid.svelte";
|
||||||
import GitManager from "$lib/components/git/GitManager.svelte";
|
import GitManager from "$lib/components/git/GitManager.svelte";
|
||||||
import { gitService } from "../../../services/gitService";
|
import { gitService } from "../../../services/gitService";
|
||||||
import { addToast } from "$lib/toasts.svelte.js";
|
import { addToast } from "$lib/toasts.svelte.js";
|
||||||
@@ -21,24 +23,19 @@
|
|||||||
|
|
||||||
// [SECTION: PROPS]
|
// [SECTION: PROPS]
|
||||||
let {
|
let {
|
||||||
dashboards = [],
|
dashboards = [] as DashboardMetadata[],
|
||||||
selectedIds = [],
|
selectedIds = [] as number[],
|
||||||
statusMode = "dashboard",
|
statusMode = "dashboard" as "dashboard" | "repository",
|
||||||
envId = null,
|
envId = null as string | null,
|
||||||
repositoriesOnly = false,
|
repositoriesOnly = false,
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [SECTION: STATE]
|
// [SECTION: STATE]
|
||||||
let filterText = $state("");
|
let filterText = $state("");
|
||||||
let currentPage = $state(0);
|
let currentPage = $state(0);
|
||||||
let pageSize = $state(20);
|
let sortColumn = $state("title");
|
||||||
let sortColumn: keyof DashboardMetadata = $state("title");
|
|
||||||
let sortDirection: "asc" | "desc" = $state("asc");
|
let sortDirection: "asc" | "desc" = $state("asc");
|
||||||
// [/SECTION]
|
|
||||||
|
|
||||||
// [SECTION: UI STATE]
|
|
||||||
let showGitManager = $state(false);
|
let showGitManager = $state(false);
|
||||||
let gitDashboardId: string | null = $state(null);
|
let gitDashboardId: string | null = $state(null);
|
||||||
let gitDashboardTitle = $state("");
|
let gitDashboardTitle = $state("");
|
||||||
@@ -48,132 +45,39 @@
|
|||||||
let showBulkDeleteConfirm = $state(false);
|
let showBulkDeleteConfirm = $state(false);
|
||||||
// [/SECTION]
|
// [/SECTION]
|
||||||
|
|
||||||
// [SECTION: DERIVED]
|
// ── Column definitions ─────────────────────────────────────────
|
||||||
let filteredDashboards = $derived(
|
let columns: Column[] = $derived([
|
||||||
dashboards.filter((d) => {
|
{ key: "title", label: $t.dashboard?.title || "Title", sortable: true },
|
||||||
const matchesTitle = d.title.toLowerCase().includes(filterText.toLowerCase());
|
{ key: "last_modified", label: $t.dashboard?.last_modified || "Last Modified", sortable: true },
|
||||||
if (!matchesTitle) return false;
|
{ key: "status", label: $t.dashboard?.status || "Status", sortable: true, raw: true, render: (item: DashboardMetadata) => renderStatusBadge(item) },
|
||||||
if (!repositoriesOnly || statusMode !== "repository") return true;
|
]);
|
||||||
return getRepositoryStatusToken(d.id) !== "no_repo";
|
|
||||||
}),
|
// ── Actions per row ────────────────────────────────────────────
|
||||||
|
let rowActions = $derived([
|
||||||
|
{
|
||||||
|
label: $t.git?.manage || "Git",
|
||||||
|
variant: "ghost" as const,
|
||||||
|
handler: (item: DashboardMetadata) => openGitManagerForDashboard(item),
|
||||||
|
condition: (item: DashboardMetadata) => statusMode === "repository",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ── Pre-filtered data (repositoriesOnly) ──────────────────────
|
||||||
|
let preFilteredData = $derived<DashboardMetadata[]>(
|
||||||
|
repositoriesOnly && statusMode === "repository"
|
||||||
|
? dashboards.filter(d => getRepositoryStatusToken(d.id) !== "no_repo")
|
||||||
|
: dashboards,
|
||||||
);
|
);
|
||||||
|
|
||||||
let sortedDashboards = $derived(
|
// [SECTION: GIT-SPECIFIC FUNCTIONS]
|
||||||
[...filteredDashboards].sort((a, b) => {
|
|
||||||
let aVal =
|
|
||||||
sortColumn === "status"
|
|
||||||
? getSortStatusValue(a)
|
|
||||||
: a[sortColumn];
|
|
||||||
let bVal =
|
|
||||||
sortColumn === "status"
|
|
||||||
? getSortStatusValue(b)
|
|
||||||
: b[sortColumn];
|
|
||||||
if (sortColumn === "id") {
|
|
||||||
aVal = Number(aVal);
|
|
||||||
bVal = Number(bVal);
|
|
||||||
}
|
|
||||||
if (aVal < bVal) return sortDirection === "asc" ? -1 : 1;
|
|
||||||
if (aVal > bVal) return sortDirection === "asc" ? 1 : -1;
|
|
||||||
return 0;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
let paginatedDashboards = $derived(
|
|
||||||
sortedDashboards.slice(
|
|
||||||
currentPage * pageSize,
|
|
||||||
(currentPage + 1) * pageSize,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
let totalPages = $derived(Math.ceil(sortedDashboards.length / pageSize));
|
|
||||||
|
|
||||||
let allSelected = $derived(
|
|
||||||
paginatedDashboards.length > 0 &&
|
|
||||||
paginatedDashboards.every((d) => selectedIds.includes(d.id)),
|
|
||||||
);
|
|
||||||
let someSelected = $derived(
|
|
||||||
paginatedDashboards.some((d) => selectedIds.includes(d.id)),
|
|
||||||
);
|
|
||||||
// [/SECTION]
|
|
||||||
|
|
||||||
// #region handleSort:Function [TYPE Function]
|
|
||||||
// @ingroup Dashboard
|
|
||||||
// @PURPOSE: Toggles sort direction or changes sort column.
|
|
||||||
// @PRE: column name is provided.
|
|
||||||
// @POST: sortColumn and sortDirection state updated.
|
|
||||||
function handleSort(column: keyof DashboardMetadata) {
|
|
||||||
if (sortColumn === column) {
|
|
||||||
sortDirection = sortDirection === "asc" ? "desc" : "asc";
|
|
||||||
} else {
|
|
||||||
sortColumn = column;
|
|
||||||
sortDirection = "asc";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// #endregion handleSort:Function
|
|
||||||
|
|
||||||
// #region handleSelectionChange:Function [TYPE Function]
|
|
||||||
// @ingroup Dashboard
|
|
||||||
// @PURPOSE: Handles individual checkbox changes.
|
|
||||||
// @PRE: dashboard ID and checked status provided.
|
|
||||||
// @POST: selectedIds array updated.
|
|
||||||
function handleSelectionChange(id: number, checked: boolean) {
|
|
||||||
let newSelected = [...selectedIds];
|
|
||||||
if (checked) {
|
|
||||||
if (!newSelected.includes(id)) newSelected.push(id);
|
|
||||||
} else {
|
|
||||||
newSelected = newSelected.filter((sid) => sid !== id);
|
|
||||||
}
|
|
||||||
selectedIds = newSelected;
|
|
||||||
}
|
|
||||||
// #endregion handleSelectionChange:Function
|
|
||||||
|
|
||||||
// #region handleSelectAll:Function [TYPE Function]
|
|
||||||
// @ingroup Dashboard
|
|
||||||
// @PURPOSE: Handles select all checkbox.
|
|
||||||
// @PRE: checked status provided.
|
|
||||||
// @POST: selectedIds array updated for all paginated items.
|
|
||||||
function handleSelectAll(checked: boolean) {
|
|
||||||
let newSelected = [...selectedIds];
|
|
||||||
if (checked) {
|
|
||||||
paginatedDashboards.forEach((d) => {
|
|
||||||
if (!newSelected.includes(d.id)) newSelected.push(d.id);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
paginatedDashboards.forEach((d) => {
|
|
||||||
newSelected = newSelected.filter((sid) => sid !== d.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
selectedIds = newSelected;
|
|
||||||
}
|
|
||||||
// #endregion handleSelectAll:Function
|
|
||||||
|
|
||||||
// #region goToPage:Function [TYPE Function]
|
|
||||||
// @ingroup Dashboard
|
|
||||||
// @PURPOSE: Changes current page.
|
|
||||||
// @PRE: page index is provided.
|
|
||||||
// @POST: currentPage state updated if within valid range.
|
|
||||||
function goToPage(page: number) {
|
|
||||||
if (page >= 0 && page < totalPages) {
|
|
||||||
currentPage = page;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// #endregion goToPage:Function
|
|
||||||
|
|
||||||
// #region getRepositoryStatusToken:Function [TYPE Function]
|
// #region getRepositoryStatusToken:Function [TYPE Function]
|
||||||
// @ingroup Dashboard
|
|
||||||
/**
|
|
||||||
* @purpose Returns normalized repository status token for a dashboard.
|
|
||||||
* @pre Dashboard exists.
|
|
||||||
* @post Returns one of loading|no_repo|synced|changes|behind_remote|ahead_remote|diverged|error.
|
|
||||||
*/
|
|
||||||
function getRepositoryStatusToken(dashboardId: number): string {
|
function getRepositoryStatusToken(dashboardId: number): string {
|
||||||
return repositoryStatusByDashboardId[dashboardId] || "loading";
|
return repositoryStatusByDashboardId[dashboardId] || "loading";
|
||||||
}
|
}
|
||||||
// #endregion getRepositoryStatusToken:Function
|
// #endregion getRepositoryStatusToken:Function
|
||||||
|
|
||||||
// #region isRepositoryReady:Function [TYPE Function]
|
// #region isRepositoryReady:Function [TYPE Function]
|
||||||
// @ingroup Dashboard
|
|
||||||
// @PURPOSE: Determines whether git actions can run for a dashboard.
|
|
||||||
function isRepositoryReady(dashboardId: number): boolean {
|
function isRepositoryReady(dashboardId: number): boolean {
|
||||||
const token = getRepositoryStatusToken(dashboardId);
|
const token = getRepositoryStatusToken(dashboardId);
|
||||||
return token !== "loading" && token !== "no_repo" && token !== "error";
|
return token !== "loading" && token !== "no_repo" && token !== "error";
|
||||||
@@ -181,23 +85,15 @@
|
|||||||
// #endregion isRepositoryReady:Function
|
// #endregion isRepositoryReady:Function
|
||||||
|
|
||||||
// #region invalidateRepositoryStatuses:Function [TYPE Function]
|
// #region invalidateRepositoryStatuses:Function [TYPE Function]
|
||||||
// @ingroup Dashboard
|
|
||||||
// @PURPOSE: Marks dashboard statuses as loading so they are refetched.
|
|
||||||
function invalidateRepositoryStatuses(dashboardIds: number[]): void {
|
function invalidateRepositoryStatuses(dashboardIds: number[]): void {
|
||||||
if (dashboardIds.length === 0) return;
|
if (dashboardIds.length === 0) return;
|
||||||
const nextStatuses = { ...repositoryStatusByDashboardId };
|
const nextStatuses = { ...repositoryStatusByDashboardId };
|
||||||
dashboardIds.forEach((dashboardId) => {
|
dashboardIds.forEach((did) => { nextStatuses[did] = "loading"; });
|
||||||
nextStatuses[dashboardId] = "loading";
|
|
||||||
});
|
|
||||||
repositoryStatusByDashboardId = nextStatuses;
|
repositoryStatusByDashboardId = nextStatuses;
|
||||||
}
|
}
|
||||||
// #endregion invalidateRepositoryStatuses:Function
|
// #endregion invalidateRepositoryStatuses:Function
|
||||||
|
|
||||||
// #region resolveRepositoryStatusToken:Function [TYPE Function]
|
// #region resolveRepositoryStatusToken:Function [TYPE Function]
|
||||||
// @ingroup Dashboard
|
|
||||||
/**
|
|
||||||
* @purpose Converts git status payload into a stable UI status token.
|
|
||||||
*/
|
|
||||||
function resolveRepositoryStatusToken(status: any): string {
|
function resolveRepositoryStatusToken(status: any): string {
|
||||||
const syncState = String(status?.sync_state || "").toUpperCase();
|
const syncState = String(status?.sync_state || "").toUpperCase();
|
||||||
if (syncState === "DIVERGED") return "diverged";
|
if (syncState === "DIVERGED") return "diverged";
|
||||||
@@ -205,81 +101,53 @@
|
|||||||
if (syncState === "AHEAD_REMOTE") return "ahead_remote";
|
if (syncState === "AHEAD_REMOTE") return "ahead_remote";
|
||||||
if (syncState === "CHANGES") return "changes";
|
if (syncState === "CHANGES") return "changes";
|
||||||
if (syncState === "SYNCED") return "synced";
|
if (syncState === "SYNCED") return "synced";
|
||||||
|
|
||||||
const syncStatus = String(status?.sync_status || "").toUpperCase();
|
const syncStatus = String(status?.sync_status || "").toUpperCase();
|
||||||
if (syncStatus === "NO_REPO") return "no_repo";
|
if (syncStatus === "NO_REPO") return "no_repo";
|
||||||
if (syncStatus === "ERROR") return "error";
|
if (syncStatus === "ERROR") return "error";
|
||||||
if (syncStatus === "DIFF") return "changes";
|
if (syncStatus === "DIFF") return "changes";
|
||||||
if (syncStatus === "OK") return "synced";
|
if (syncStatus === "OK") return "synced";
|
||||||
|
|
||||||
const aheadCount = Number(status?.ahead_count || 0);
|
const aheadCount = Number(status?.ahead_count || 0);
|
||||||
const behindCount = Number(status?.behind_count || 0);
|
const behindCount = Number(status?.behind_count || 0);
|
||||||
if (aheadCount > 0 && behindCount > 0) return "diverged";
|
if (aheadCount > 0 && behindCount > 0) return "diverged";
|
||||||
if (behindCount > 0) return "behind_remote";
|
if (behindCount > 0) return "behind_remote";
|
||||||
if (aheadCount > 0) return "ahead_remote";
|
if (aheadCount > 0) return "ahead_remote";
|
||||||
|
|
||||||
const hasChanges =
|
const hasChanges =
|
||||||
Boolean(status?.is_dirty) ||
|
Boolean(status?.is_dirty) ||
|
||||||
(status?.untracked_files?.length || 0) > 0 ||
|
(status?.untracked_files?.length || 0) > 0 ||
|
||||||
(status?.modified_files?.length || 0) > 0 ||
|
(status?.modified_files?.length || 0) > 0 ||
|
||||||
(status?.staged_files?.length || 0) > 0;
|
(status?.staged_files?.length || 0) > 0;
|
||||||
|
|
||||||
return hasChanges ? "changes" : "synced";
|
return hasChanges ? "changes" : "synced";
|
||||||
}
|
}
|
||||||
// #endregion resolveRepositoryStatusToken:Function
|
// #endregion resolveRepositoryStatusToken:Function
|
||||||
|
|
||||||
// #region loadRepositoryStatuses:Function [TYPE Function]
|
// #region loadRepositoryStatuses:Function [TYPE Function]
|
||||||
// @ingroup Dashboard
|
|
||||||
/**
|
|
||||||
* @purpose Hydrates repository status map for dashboards in repository mode.
|
|
||||||
*/
|
|
||||||
async function loadRepositoryStatuses() {
|
async function loadRepositoryStatuses() {
|
||||||
if (statusMode !== "repository") {
|
if (statusMode !== "repository") {
|
||||||
repositoryStatusByDashboardId = {};
|
repositoryStatusByDashboardId = {};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const requestId = ++repositoryStatusRequestId;
|
||||||
const requestId = repositoryStatusRequestId + 1;
|
// Load statuses for all pre-filtered data (not just current page — DashboardDataGrid owns pagination)
|
||||||
repositoryStatusRequestId = requestId;
|
const allIds = dashboards.map(d => d.id);
|
||||||
|
const missingIds = allIds.filter(id => {
|
||||||
const visibleDashboards = paginatedDashboards;
|
const tkn = repositoryStatusByDashboardId[id];
|
||||||
const visibleIds = visibleDashboards.map((dashboard) => dashboard.id);
|
return tkn === undefined || tkn === "loading";
|
||||||
|
|
||||||
if (visibleDashboards.length === 0) {
|
|
||||||
repositoryStatusByDashboardId = {};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const missingIds = visibleIds.filter((dashboardId) => {
|
|
||||||
const token = repositoryStatusByDashboardId[dashboardId];
|
|
||||||
return token === undefined || token === "loading";
|
|
||||||
});
|
});
|
||||||
|
if (missingIds.length === 0) return;
|
||||||
if (missingIds.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadingStatuses = { ...repositoryStatusByDashboardId };
|
const loadingStatuses = { ...repositoryStatusByDashboardId };
|
||||||
missingIds.forEach((dashboardId) => {
|
missingIds.forEach(id => { loadingStatuses[id] = "loading"; });
|
||||||
loadingStatuses[dashboardId] = "loading";
|
|
||||||
});
|
|
||||||
repositoryStatusByDashboardId = loadingStatuses;
|
repositoryStatusByDashboardId = loadingStatuses;
|
||||||
|
|
||||||
let entries: Array<readonly [number, string]> = [];
|
let entries: Array<readonly [number, string]> = [];
|
||||||
try {
|
try {
|
||||||
const batchResult = await gitService.getStatusesBatch(missingIds);
|
const batchResult = await gitService.getStatusesBatch(missingIds);
|
||||||
const statusesByDashboardId = batchResult?.statuses || {};
|
const statusesByDashboardId = batchResult?.statuses || {};
|
||||||
entries = missingIds.map((dashboardId) => {
|
entries = missingIds.map((id) => {
|
||||||
const status =
|
const s = statusesByDashboardId[id] ?? statusesByDashboardId[String(id)];
|
||||||
statusesByDashboardId[dashboardId] ??
|
return [id, s ? resolveRepositoryStatusToken(s) : "error"] as const;
|
||||||
statusesByDashboardId[String(dashboardId)];
|
|
||||||
if (!status) return [dashboardId, "error"] as const;
|
|
||||||
return [dashboardId, resolveRepositoryStatusToken(status)] as const;
|
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
entries = missingIds.map((dashboardId) => [dashboardId, "error"] as const);
|
entries = missingIds.map((id) => [id, "error"] as const);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestId !== repositoryStatusRequestId) return;
|
if (requestId !== repositoryStatusRequestId) return;
|
||||||
repositoryStatusByDashboardId = {
|
repositoryStatusByDashboardId = {
|
||||||
...repositoryStatusByDashboardId,
|
...repositoryStatusByDashboardId,
|
||||||
@@ -289,424 +157,191 @@
|
|||||||
// #endregion loadRepositoryStatuses:Function
|
// #endregion loadRepositoryStatuses:Function
|
||||||
|
|
||||||
// #region runBulkGitAction:Function [TYPE Function]
|
// #region runBulkGitAction:Function [TYPE Function]
|
||||||
// @ingroup Dashboard
|
async function runBulkGitAction(actionToken: string, action: (_id: number) => Promise<void>): Promise<void> {
|
||||||
// @PURPOSE: Executes git action for selected dashboards with limited parallelism.
|
|
||||||
async function runBulkGitAction(
|
|
||||||
actionToken: string,
|
|
||||||
action: (_dashboardId: number) => Promise<void>,
|
|
||||||
): Promise<void> {
|
|
||||||
if (bulkActionRunning) return;
|
if (bulkActionRunning) return;
|
||||||
const selectedDashboardIds = selectedIds.filter((dashboardId) =>
|
const readyIds = selectedIds.filter((id) => isRepositoryReady(id));
|
||||||
isRepositoryReady(dashboardId),
|
if (readyIds.length === 0) { addToast($t.git?.no_repositories_selected, "error"); return; }
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedDashboardIds.length === 0) {
|
|
||||||
addToast($t.git?.no_repositories_selected, "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bulkActionRunning = true;
|
bulkActionRunning = true;
|
||||||
const concurrency = 3;
|
const concurrency = 3;
|
||||||
const idsQueue = [...selectedDashboardIds];
|
const queue = [...readyIds];
|
||||||
let successCount = 0;
|
let successCount = 0, failedCount = 0;
|
||||||
let failedCount = 0;
|
|
||||||
|
|
||||||
const worker = async () => {
|
const worker = async () => {
|
||||||
while (idsQueue.length > 0) {
|
while (queue.length > 0) {
|
||||||
const dashboardId = idsQueue.shift();
|
const id = queue.shift();
|
||||||
if (dashboardId === undefined) break;
|
if (id === undefined) break;
|
||||||
try {
|
try { await action(id); successCount++; }
|
||||||
await action(dashboardId);
|
catch { failedCount++; }
|
||||||
successCount += 1;
|
|
||||||
} catch {
|
|
||||||
failedCount += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(
|
await Promise.all(Array.from({ length: Math.min(concurrency, readyIds.length) }, () => worker()));
|
||||||
Array.from({ length: Math.min(concurrency, selectedDashboardIds.length) }, () => worker()),
|
invalidateRepositoryStatuses(readyIds);
|
||||||
);
|
const al = $t.git?.[`bulk_action_${actionToken}`] || actionToken;
|
||||||
invalidateRepositoryStatuses(selectedDashboardIds);
|
addToast($t.git?.bulk_result.replace("{action}", al).replace("{success}", String(successCount)).replace("{failed}", String(failedCount)),
|
||||||
const actionLabel = $t.git?.[`bulk_action_${actionToken}`] || actionToken;
|
failedCount > 0 ? "warning" : "success");
|
||||||
addToast(
|
} finally { bulkActionRunning = false; }
|
||||||
$t.git?.bulk_result
|
|
||||||
.replace("{action}", actionLabel)
|
|
||||||
.replace("{success}", String(successCount))
|
|
||||||
.replace("{failed}", String(failedCount)),
|
|
||||||
failedCount > 0 ? "warning" : "success",
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
bulkActionRunning = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// #endregion runBulkGitAction:Function
|
// #endregion runBulkGitAction:Function
|
||||||
|
|
||||||
// #region handleBulkSync:Function [TYPE Function]
|
// #region handleBulkAction:Function [TYPE Function]
|
||||||
// @ingroup Dashboard
|
async function handleBulkSync() { await runBulkGitAction("sync", (id) => gitService.sync(id, null, envId)); }
|
||||||
async function handleBulkSync(): Promise<void> {
|
async function handleBulkCommit() {
|
||||||
await runBulkGitAction("sync", (dashboardId) => gitService.sync(dashboardId, null, envId));
|
const msg = prompt($t.git?.commit_message);
|
||||||
|
if (!msg?.trim()) return;
|
||||||
|
await runBulkGitAction("commit", (id) => gitService.commit(id, msg.trim(), [], envId));
|
||||||
}
|
}
|
||||||
// #endregion handleBulkSync:Function
|
async function handleBulkPull() { await runBulkGitAction("pull", (id) => gitService.pull(id, envId)); }
|
||||||
|
async function handleBulkPush() { await runBulkGitAction("push", (id) => gitService.push(id, envId)); }
|
||||||
// #region handleBulkCommit:Function [TYPE Function]
|
async function onConfirmBulkDelete() {
|
||||||
// @ingroup Dashboard
|
|
||||||
async function handleBulkCommit(): Promise<void> {
|
|
||||||
const message = prompt($t.git?.commit_message);
|
|
||||||
if (!message?.trim()) return;
|
|
||||||
await runBulkGitAction("commit", (dashboardId) =>
|
|
||||||
gitService.commit(dashboardId, message.trim(), [], envId),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// #endregion handleBulkCommit:Function
|
|
||||||
|
|
||||||
// #region handleBulkPull:Function [TYPE Function]
|
|
||||||
// @ingroup Dashboard
|
|
||||||
async function handleBulkPull(): Promise<void> {
|
|
||||||
await runBulkGitAction("pull", (dashboardId) => gitService.pull(dashboardId, envId));
|
|
||||||
}
|
|
||||||
// #endregion handleBulkPull:Function
|
|
||||||
|
|
||||||
// #region handleBulkPush:Function [TYPE Function]
|
|
||||||
// @ingroup Dashboard
|
|
||||||
async function handleBulkPush(): Promise<void> {
|
|
||||||
await runBulkGitAction("push", (dashboardId) => gitService.push(dashboardId, envId));
|
|
||||||
}
|
|
||||||
// #endregion handleBulkPush:Function
|
|
||||||
|
|
||||||
// #region handleBulkDelete:Function [TYPE Function]
|
|
||||||
// @ingroup Dashboard
|
|
||||||
// @PURPOSE: Removes selected repositories from storage and binding table.
|
|
||||||
function handleBulkDelete(): void {
|
|
||||||
showBulkDeleteConfirm = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onConfirmBulkDelete(): Promise<void> {
|
|
||||||
const idsToDelete = [...selectedIds];
|
const idsToDelete = [...selectedIds];
|
||||||
await runBulkGitAction("delete", (dashboardId) =>
|
await runBulkGitAction("delete", (id) => gitService.deleteRepository(id, envId));
|
||||||
gitService.deleteRepository(dashboardId, envId),
|
dashboards = dashboards.filter((d) => !idsToDelete.includes(d.id));
|
||||||
);
|
|
||||||
dashboards = dashboards.filter((dashboard) => !idsToDelete.includes(dashboard.id));
|
|
||||||
selectedIds = [];
|
selectedIds = [];
|
||||||
}
|
}
|
||||||
// #endregion handleBulkDelete:Function
|
// #endregion handleBulkAction:Function
|
||||||
|
|
||||||
// #region handleManageSelected:Function [TYPE Function]
|
// #region handleManageSelected:Function [TYPE Function]
|
||||||
// @ingroup Dashboard
|
function handleManageSelected(): void {
|
||||||
// @PURPOSE: Opens Git manager for exactly one selected dashboard.
|
if (selectedIds.length !== 1) { addToast($t.git?.select_single_for_manage, "warning"); return; }
|
||||||
async function handleManageSelected(): Promise<void> {
|
const d = dashboards.find(dash => dash.id === selectedIds[0]);
|
||||||
if (selectedIds.length !== 1) {
|
openGitManagerForDashboard(d || null);
|
||||||
addToast($t.git?.select_single_for_manage, "warning");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedDashboardId = selectedIds[0];
|
|
||||||
const selectedDashboard = dashboards.find(
|
|
||||||
(dashboard) => dashboard.id === selectedDashboardId,
|
|
||||||
);
|
|
||||||
openGitManagerForDashboard(selectedDashboard || null);
|
|
||||||
}
|
}
|
||||||
// #endregion handleManageSelected:Function
|
// #endregion handleManageSelected:Function
|
||||||
|
|
||||||
// #region resolveDashboardRef:Function [TYPE Function]
|
// #region handleInitializeRepositories:Function [TYPE Function]
|
||||||
// @ingroup Dashboard
|
function handleInitializeRepositories(): void {
|
||||||
// @PURPOSE: Resolves dashboard slug from payload fields.
|
if (selectedIds.length !== 1) { addToast($t.git?.select_single_for_manage, "warning"); return; }
|
||||||
// @PRE: Dashboard metadata is provided.
|
const d = dashboards.find(dash => dash.id === selectedIds[0]) || null;
|
||||||
// @POST: Returns slug string or null if unavailable.
|
openGitManagerForDashboard(d);
|
||||||
function resolveDashboardRef(dashboard: DashboardMetadata): string | null {
|
}
|
||||||
const directSlug = String(
|
// #endregion handleInitializeRepositories:Function
|
||||||
dashboard.slug ||
|
|
||||||
dashboard.dashboard_slug ||
|
|
||||||
dashboard.url_slug ||
|
|
||||||
"",
|
|
||||||
).trim();
|
|
||||||
if (directSlug) return directSlug;
|
|
||||||
|
|
||||||
|
// #region resolveDashboardRef:Function [TYPE Function]
|
||||||
|
function resolveDashboardRef(dashboard: DashboardMetadata): string | null {
|
||||||
|
const directSlug = String(dashboard.slug || dashboard.dashboard_slug || dashboard.url_slug || "").trim();
|
||||||
|
if (directSlug) return directSlug;
|
||||||
const dashboardUrl = String(dashboard.url || "").trim();
|
const dashboardUrl = String(dashboard.url || "").trim();
|
||||||
if (!dashboardUrl) return null;
|
if (!dashboardUrl) return null;
|
||||||
const slugMatch = dashboardUrl.match(/\/dashboard\/([^/?#]+)/i);
|
const slugMatch = dashboardUrl.match(/\/dashboard\/([^/?#]+)/i);
|
||||||
if (!slugMatch?.[1]) return null;
|
return slugMatch ? decodeURIComponent(slugMatch[1]) : null;
|
||||||
return decodeURIComponent(slugMatch[1]);
|
|
||||||
}
|
}
|
||||||
// #endregion resolveDashboardRef:Function
|
// #endregion resolveDashboardRef:Function
|
||||||
|
|
||||||
// #region openGitManagerForDashboard:Function [TYPE Function]
|
// #region openGitManagerForDashboard:Function [TYPE Function]
|
||||||
// @ingroup Dashboard
|
|
||||||
// @PURPOSE: Opens Git manager for provided dashboard metadata.
|
|
||||||
function openGitManagerForDashboard(dashboard: DashboardMetadata | null): void {
|
function openGitManagerForDashboard(dashboard: DashboardMetadata | null): void {
|
||||||
if (!dashboard) return;
|
if (!dashboard) return;
|
||||||
const dashboardRef = resolveDashboardRef(dashboard);
|
const ref = resolveDashboardRef(dashboard);
|
||||||
if (!dashboardRef) {
|
if (!ref) { addToast($t.git?.select_dashboard_with_slug || "Dashboard slug required", "error"); return; }
|
||||||
addToast($t.git?.select_dashboard_with_slug || "Dashboard slug is required to open GitManager", "error");
|
gitDashboardId = ref;
|
||||||
return;
|
|
||||||
}
|
|
||||||
gitDashboardId = dashboardRef;
|
|
||||||
gitDashboardTitle = dashboard.title || "";
|
gitDashboardTitle = dashboard.title || "";
|
||||||
showGitManager = true;
|
showGitManager = true;
|
||||||
}
|
}
|
||||||
// #endregion openGitManagerForDashboard:Function
|
// #endregion openGitManagerForDashboard:Function
|
||||||
|
|
||||||
// #region handleInitializeRepositories:Function [TYPE Function]
|
|
||||||
// @ingroup Dashboard
|
|
||||||
// @PURPOSE: Opens Git manager from bulk actions to initialize selected repository.
|
|
||||||
async function handleInitializeRepositories(): Promise<void> {
|
|
||||||
if (selectedIds.length !== 1) {
|
|
||||||
addToast($t.git?.select_single_for_manage, "warning");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const selectedDashboardId = selectedIds[0];
|
|
||||||
const selectedDashboard = dashboards.find((dashboard) => dashboard.id === selectedDashboardId) || null;
|
|
||||||
openGitManagerForDashboard(selectedDashboard);
|
|
||||||
}
|
|
||||||
// #endregion handleInitializeRepositories:Function
|
|
||||||
|
|
||||||
// #region getSortStatusValue:Function [TYPE Function]
|
// #region getSortStatusValue:Function [TYPE Function]
|
||||||
// @ingroup Dashboard
|
|
||||||
/**
|
|
||||||
* @purpose Returns sort value for status column based on mode.
|
|
||||||
*/
|
|
||||||
function getSortStatusValue(dashboard: DashboardMetadata): string {
|
function getSortStatusValue(dashboard: DashboardMetadata): string {
|
||||||
if (statusMode === "repository") {
|
if (statusMode === "repository") return getRepositoryStatusToken(dashboard.id);
|
||||||
return getRepositoryStatusToken(dashboard.id);
|
|
||||||
}
|
|
||||||
return String(dashboard.status || "").toLowerCase();
|
return String(dashboard.status || "").toLowerCase();
|
||||||
}
|
}
|
||||||
// #endregion getSortStatusValue:Function
|
// #endregion getSortStatusValue:Function
|
||||||
|
|
||||||
// #region getStatusLabel:Function [TYPE Function]
|
// #region getStatusLabel:Function [TYPE Function]
|
||||||
// @ingroup Dashboard
|
|
||||||
/**
|
|
||||||
* @purpose Returns localized label for status column.
|
|
||||||
*/
|
|
||||||
function getStatusLabel(dashboard: DashboardMetadata): string {
|
function getStatusLabel(dashboard: DashboardMetadata): string {
|
||||||
if (statusMode !== "repository") {
|
if (statusMode !== "repository") return String(dashboard.status || "");
|
||||||
return String(dashboard.status || "");
|
|
||||||
}
|
|
||||||
const token = getRepositoryStatusToken(dashboard.id);
|
const token = getRepositoryStatusToken(dashboard.id);
|
||||||
return $t.git?.repo_status?.[token] || token;
|
return $t.git?.repo_status?.[token] || token;
|
||||||
}
|
}
|
||||||
// #endregion getStatusLabel:Function
|
// #endregion getStatusLabel:Function
|
||||||
|
|
||||||
// #region getStatusBadgeClass:Function [TYPE Function]
|
// #region getStatusBadgeClass:Function [TYPE Function]
|
||||||
// @ingroup Dashboard
|
|
||||||
/**
|
|
||||||
* @purpose Returns badge style for status column.
|
|
||||||
*/
|
|
||||||
function getStatusBadgeClass(dashboard: DashboardMetadata): string {
|
function getStatusBadgeClass(dashboard: DashboardMetadata): string {
|
||||||
if (statusMode !== "repository") {
|
if (statusMode !== "repository") {
|
||||||
return dashboard.status === "published"
|
return dashboard.status === "published" ? "bg-success-light text-success" : "bg-surface-muted text-text";
|
||||||
? "bg-success-light text-success"
|
|
||||||
: "bg-surface-muted text-text";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = getRepositoryStatusToken(dashboard.id);
|
const token = getRepositoryStatusToken(dashboard.id);
|
||||||
if (token === "loading") return "bg-surface-muted text-text-muted";
|
if (token === "loading" || token === "no_repo") return "bg-surface-muted text-text-muted";
|
||||||
if (token === "no_repo") return "bg-surface-muted text-text";
|
|
||||||
if (token === "synced") return "bg-success-light text-success";
|
if (token === "synced") return "bg-success-light text-success";
|
||||||
if (token === "changes") return "bg-warning-light text-warning";
|
if (token === "changes") return "bg-warning-light text-warning";
|
||||||
if (token === "behind_remote") return "bg-primary-light text-primary";
|
if (token === "behind_remote" || token === "ahead_remote") return "bg-primary-light text-primary";
|
||||||
if (token === "ahead_remote") return "bg-primary-light text-primary";
|
|
||||||
if (token === "diverged") return "bg-info-light text-info";
|
if (token === "diverged") return "bg-info-light text-info";
|
||||||
return "bg-destructive-light text-destructive";
|
return "bg-destructive-light text-destructive";
|
||||||
}
|
}
|
||||||
// #endregion getStatusBadgeClass:Function
|
// #endregion getStatusBadgeClass:Function
|
||||||
|
|
||||||
|
// #region renderStatusBadge:Function [TYPE Function]
|
||||||
|
function renderStatusBadge(dashboard: DashboardMetadata): string {
|
||||||
|
const classes = getStatusBadgeClass(dashboard);
|
||||||
|
const label = getStatusLabel(dashboard);
|
||||||
|
return `<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${classes}">${label}</span>`;
|
||||||
|
}
|
||||||
|
// #endregion renderStatusBadge:Function
|
||||||
|
|
||||||
|
// ── Reactive: load repository statuses ─────────────────────────
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
console.count("[Grid.effect491]");
|
|
||||||
dashboards;
|
dashboards;
|
||||||
statusMode;
|
statusMode;
|
||||||
currentPage;
|
untrack(() => { void loadRepositoryStatuses(); });
|
||||||
pageSize;
|
|
||||||
filterText;
|
|
||||||
sortColumn;
|
|
||||||
sortDirection;
|
|
||||||
untrack(() => {
|
|
||||||
void loadRepositoryStatuses();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- [SECTION: TEMPLATE] -->
|
<!-- [SECTION: TEMPLATE] -->
|
||||||
<div class="dashboard-grid">
|
<div class="repository-dashboard-grid">
|
||||||
<!-- Filter Input -->
|
<DashboardDataGrid
|
||||||
<div class="mb-6">
|
data={preFilteredData}
|
||||||
<Input bind:value={filterText} placeholder={$t.dashboard.search} />
|
{columns}
|
||||||
</div>
|
selectable
|
||||||
|
bind:selectedIds
|
||||||
{#if selectedIds.length > 0}
|
sortable
|
||||||
<div class="mb-4 flex flex-wrap items-center gap-2 rounded-lg border border-primary-ring bg-primary-light/60 px-3 py-2">
|
bind:sortColumn
|
||||||
{#if !repositoriesOnly}
|
bind:sortDirection
|
||||||
<Button
|
filterable
|
||||||
size="sm"
|
bind:filterText
|
||||||
variant="secondary"
|
paginated
|
||||||
onclick={handleManageSelected}
|
bind:page={currentPage}
|
||||||
disabled={bulkActionRunning || selectedIds.length !== 1}
|
pageSize={20}
|
||||||
class="border-primary-ring bg-surface-card text-primary hover:bg-primary-light disabled:opacity-40"
|
filterPlaceholder={$t.dashboard?.search}
|
||||||
>
|
actions={rowActions}
|
||||||
{$t.git?.manage_selected}
|
loading={bulkActionRunning}
|
||||||
</Button>
|
>
|
||||||
<Button
|
{#snippet children()}
|
||||||
size="sm"
|
{#if selectedIds.length > 0}
|
||||||
variant="secondary"
|
<div class="flex flex-wrap items-center gap-2 rounded-lg border border-primary-ring bg-primary-light/60 px-3 py-2">
|
||||||
onclick={handleInitializeRepositories}
|
{#if !repositoriesOnly}
|
||||||
disabled={bulkActionRunning || selectedIds.length !== 1}
|
<Button size="sm" variant="secondary" onclick={handleManageSelected}
|
||||||
class="border-border bg-surface-card text-text hover:bg-surface-page disabled:opacity-40"
|
disabled={bulkActionRunning || selectedIds.length !== 1}>
|
||||||
>
|
{$t.git?.manage_selected}
|
||||||
{$t.git?.init_repo || "Инициализировать Git-репозиторий"}
|
</Button>
|
||||||
</Button>
|
<Button size="sm" variant="secondary" onclick={handleInitializeRepositories}
|
||||||
|
disabled={bulkActionRunning || selectedIds.length !== 1}>
|
||||||
|
{$t.git?.init_repo || "Init Git"}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<Button size="sm" variant="secondary" onclick={handleBulkSync} disabled={bulkActionRunning}>
|
||||||
|
{$t.git?.bulk_sync}
|
||||||
|
</Button>
|
||||||
|
{#if !repositoriesOnly}
|
||||||
|
<Button size="sm" variant="secondary" onclick={handleBulkCommit} disabled={bulkActionRunning}>
|
||||||
|
{$t.git?.bulk_commit}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<Button size="sm" variant="secondary" onclick={handleBulkPull} disabled={bulkActionRunning}>
|
||||||
|
{$t.git?.bulk_pull}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="secondary" onclick={handleBulkPush} disabled={bulkActionRunning}>
|
||||||
|
{$t.git?.bulk_push}
|
||||||
|
</Button>
|
||||||
|
{#if repositoriesOnly}
|
||||||
|
<Button size="sm" variant="destructive" onclick={() => showBulkDeleteConfirm = true} disabled={bulkActionRunning}>
|
||||||
|
{$t.git?.delete_repo || "Delete"}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<span class="ml-1 text-xs font-medium text-text-muted">
|
||||||
|
{$t.git?.selected_count.replace("{count}", String(selectedIds.length))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<Button size="sm" variant="secondary" onclick={handleBulkSync} disabled={bulkActionRunning} class="border-primary-ring bg-surface-card text-primary hover:bg-primary-light">
|
{/snippet}
|
||||||
{$t.git?.bulk_sync}
|
</DashboardDataGrid>
|
||||||
</Button>
|
|
||||||
{#if !repositoriesOnly}
|
|
||||||
<Button size="sm" variant="secondary" onclick={handleBulkCommit} disabled={bulkActionRunning} class="border-warning bg-surface-card text-warning hover:bg-warning-light">
|
|
||||||
{$t.git?.bulk_commit}
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
<Button size="sm" variant="secondary" onclick={handleBulkPull} disabled={bulkActionRunning} class="border-cyan-200 bg-surface-card text-info hover:bg-info-light">
|
|
||||||
{$t.git?.bulk_pull}
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="secondary" onclick={handleBulkPush} disabled={bulkActionRunning} class="border-primary-ring bg-surface-card text-primary hover:bg-primary-light">
|
|
||||||
{$t.git?.bulk_push}
|
|
||||||
</Button>
|
|
||||||
{#if repositoriesOnly}
|
|
||||||
<Button size="sm" variant="secondary" onclick={handleBulkDelete} disabled={bulkActionRunning} class="border-destructive-ring bg-surface-card text-destructive hover:bg-destructive-light">
|
|
||||||
{$t.git?.delete_repo || "Удалить репозиторий"}
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
<span class="ml-1 text-xs font-medium text-text-muted">
|
|
||||||
{$t.git?.selected_count.replace(
|
|
||||||
"{count}",
|
|
||||||
String(selectedIds.length),
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Grid/Table -->
|
|
||||||
<div class="overflow-x-auto rounded-lg border border-border">
|
|
||||||
<table class="min-w-full divide-y divide-border">
|
|
||||||
<thead class="bg-surface-muted">
|
|
||||||
<tr>
|
|
||||||
<th class="px-6 py-3 text-left">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={allSelected}
|
|
||||||
indeterminate={someSelected && !allSelected}
|
|
||||||
onchange={(e) =>
|
|
||||||
handleSelectAll((e.target as HTMLInputElement).checked)}
|
|
||||||
class="h-4 w-4 text-primary border-border-strong rounded focus-visible:ring-primary-ring"
|
|
||||||
/>
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
class="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider cursor-pointer hover:text-text transition-colors"
|
|
||||||
onclick={() => handleSort("title")}
|
|
||||||
>
|
|
||||||
{$t.dashboard.title}
|
|
||||||
{sortColumn === "title"
|
|
||||||
? sortDirection === "asc"
|
|
||||||
? "↑"
|
|
||||||
: "↓"
|
|
||||||
: ""}
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
class="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider cursor-pointer hover:text-text transition-colors"
|
|
||||||
onclick={() => handleSort("last_modified")}
|
|
||||||
>
|
|
||||||
{$t.dashboard.last_modified}
|
|
||||||
{sortColumn === "last_modified"
|
|
||||||
? sortDirection === "asc"
|
|
||||||
? "↑"
|
|
||||||
: "↓"
|
|
||||||
: ""}
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
class="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider cursor-pointer hover:text-text transition-colors"
|
|
||||||
onclick={() => handleSort("status")}
|
|
||||||
>
|
|
||||||
{$t.dashboard.status}
|
|
||||||
{sortColumn === "status"
|
|
||||||
? sortDirection === "asc"
|
|
||||||
? "↑"
|
|
||||||
: "↓"
|
|
||||||
: ""}
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="bg-surface-card divide-y divide-border">
|
|
||||||
{#each paginatedDashboards as dashboard (dashboard.id)}
|
|
||||||
<tr class="hover:bg-surface-muted transition-colors">
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={selectedIds.includes(dashboard.id)}
|
|
||||||
onchange={(e) =>
|
|
||||||
handleSelectionChange(
|
|
||||||
dashboard.id,
|
|
||||||
(e.target as HTMLInputElement).checked,
|
|
||||||
)}
|
|
||||||
class="h-4 w-4 text-primary border-border-strong rounded focus-visible:ring-primary-ring"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-text">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="text-left text-primary hover:text-primary hover:underline"
|
|
||||||
onclick={() => openGitManagerForDashboard(dashboard)}
|
|
||||||
>
|
|
||||||
{dashboard.title}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-text-muted"
|
|
||||||
>{new Date(dashboard.last_modified).toLocaleDateString()}</td
|
|
||||||
>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-text-muted">
|
|
||||||
<span
|
|
||||||
class="px-2 py-1 text-xs font-medium rounded-full {getStatusBadgeClass(dashboard)}"
|
|
||||||
>
|
|
||||||
{getStatusLabel(dashboard)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pagination Controls -->
|
|
||||||
<div class="flex items-center justify-between mt-6">
|
|
||||||
<div class="text-sm text-text-muted">
|
|
||||||
{($t.dashboard?.showing ?? '')
|
|
||||||
.replace("{start}", (currentPage * pageSize + 1).toString())
|
|
||||||
.replace(
|
|
||||||
"{end}",
|
|
||||||
Math.min(
|
|
||||||
(currentPage + 1) * pageSize,
|
|
||||||
sortedDashboards.length,
|
|
||||||
).toString(),
|
|
||||||
)
|
|
||||||
.replace("{total}", sortedDashboards.length.toString())}
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
disabled={currentPage === 0}
|
|
||||||
onclick={() => goToPage(currentPage - 1)}
|
|
||||||
>
|
|
||||||
{$t.dashboard.previous}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
disabled={currentPage >= totalPages - 1}
|
|
||||||
onclick={() => goToPage(currentPage + 1)}
|
|
||||||
>
|
|
||||||
{$t.dashboard.next}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showGitManager && gitDashboardId}
|
{#if showGitManager && gitDashboardId}
|
||||||
@@ -721,7 +356,7 @@
|
|||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
bind:show={showBulkDeleteConfirm}
|
bind:show={showBulkDeleteConfirm}
|
||||||
title="Delete repositories?"
|
title="Delete repositories?"
|
||||||
message={$t.git?.confirm_delete_repo || "Удалить выбранные репозитории?"}
|
message={$t.git?.confirm_delete_repo || "Delete selected repositories?"}
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
confirmLabel="Delete"
|
confirmLabel="Delete"
|
||||||
cancelLabel="Cancel"
|
cancelLabel="Cancel"
|
||||||
@@ -729,6 +364,4 @@
|
|||||||
onCancel={() => {}}
|
onCancel={() => {}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- [/SECTION] -->
|
|
||||||
|
|
||||||
<!-- #endregion Dashboard.RepositoryGrid -->
|
<!-- #endregion Dashboard.RepositoryGrid -->
|
||||||
|
|||||||
Reference in New Issue
Block a user