- Added a new function for determining job status badge variants to improve UI consistency. - Refactored imports to utilize the core UI library for better maintainability. - Updated job report and filters components to enhance user experience and streamline functionality. - Improved layout and responsiveness across operations-related components.
214 lines
7.0 KiB
Svelte
214 lines
7.0 KiB
Svelte
<script lang="ts">
|
||
import type { RevisionDiff, RevisionPrefix, RevisionRow } from '$lib/api/types.js';
|
||
import { Button } from '$lib/ui/core/button/index.js';
|
||
import {
|
||
Card,
|
||
CardContent,
|
||
CardDescription,
|
||
CardHeader,
|
||
CardTitle
|
||
} from '$lib/ui/core/card/index.js';
|
||
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js';
|
||
import { formatDateTime } from '$lib/modules/display.js';
|
||
import { sortRevisionDiffItems } from '$lib/sort-prefixes.js';
|
||
|
||
type Props = {
|
||
revisions: RevisionRow[];
|
||
diffRevA: string;
|
||
diffRevB: string;
|
||
diffData: RevisionDiff | null;
|
||
diffLoading: boolean;
|
||
onDiffRevAChange: (value: string) => void;
|
||
onDiffRevBChange: (value: string) => void;
|
||
onLoadDiff: () => void;
|
||
};
|
||
|
||
let {
|
||
revisions,
|
||
diffRevA,
|
||
diffRevB,
|
||
diffData,
|
||
diffLoading,
|
||
onDiffRevAChange,
|
||
onDiffRevBChange,
|
||
onLoadDiff
|
||
}: Props = $props();
|
||
|
||
function revisionLabel(rev: RevisionRow): string {
|
||
return `${rev.id.slice(0, 8)}… (${formatDateTime(rev.created_at)})`;
|
||
}
|
||
|
||
function diffAddedRaw(d: RevisionDiff | null): (string | RevisionPrefix)[] {
|
||
if (!d) return [];
|
||
if (d.prefixes && Array.isArray(d.prefixes.added)) return d.prefixes.added;
|
||
if (Array.isArray(d.added)) return d.added;
|
||
return [];
|
||
}
|
||
|
||
function diffRemovedRaw(d: RevisionDiff | null): (string | RevisionPrefix)[] {
|
||
if (!d) return [];
|
||
if (d.prefixes && Array.isArray(d.prefixes.removed)) return d.prefixes.removed;
|
||
if (Array.isArray(d.removed)) return d.removed;
|
||
return [];
|
||
}
|
||
|
||
let addedSorted = $derived(diffData ? sortRevisionDiffItems(diffAddedRaw(diffData)) : []);
|
||
let removedSorted = $derived(diffData ? sortRevisionDiffItems(diffRemovedRaw(diffData)) : []);
|
||
|
||
let scrollAddedEl: HTMLDivElement | null = $state(null);
|
||
let scrollRemovedEl: HTMLDivElement | null = $state(null);
|
||
let syncScrollLock = false;
|
||
|
||
function syncFromAdded() {
|
||
if (syncScrollLock || !scrollAddedEl || !scrollRemovedEl) return;
|
||
syncScrollLock = true;
|
||
scrollRemovedEl.scrollTop = scrollAddedEl.scrollTop;
|
||
queueMicrotask(() => {
|
||
syncScrollLock = false;
|
||
});
|
||
}
|
||
|
||
function syncFromRemoved() {
|
||
if (syncScrollLock || !scrollAddedEl || !scrollRemovedEl) return;
|
||
syncScrollLock = true;
|
||
scrollAddedEl.scrollTop = scrollRemovedEl.scrollTop;
|
||
queueMicrotask(() => {
|
||
syncScrollLock = false;
|
||
});
|
||
}
|
||
</script>
|
||
|
||
<Card class="min-h-0">
|
||
<CardHeader>
|
||
<CardTitle class="text-base">Сравнение ревизий</CardTitle>
|
||
<CardDescription>Выберите ID двух ревизий для сравнения</CardDescription>
|
||
</CardHeader>
|
||
<CardContent class="min-h-0 space-y-4">
|
||
<div class="flex flex-col gap-2 sm:flex-row">
|
||
<Select type="single" value={diffRevA} onValueChange={(v) => onDiffRevAChange(v ?? '')}>
|
||
<SelectTrigger class="min-w-0 flex-1">
|
||
{diffRevA
|
||
? revisions.find((r) => r.id === diffRevA)
|
||
? revisionLabel(revisions.find((r) => r.id === diffRevA)!)
|
||
: diffRevA
|
||
: 'Ревизия A'}
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="">Ревизия A</SelectItem>
|
||
{#each revisions as rev (rev.id)}
|
||
<SelectItem value={rev.id}>{revisionLabel(rev)}</SelectItem>
|
||
{/each}
|
||
</SelectContent>
|
||
</Select>
|
||
<Select type="single" value={diffRevB} onValueChange={(v) => onDiffRevBChange(v ?? '')}>
|
||
<SelectTrigger class="min-w-0 flex-1">
|
||
{diffRevB
|
||
? revisions.find((r) => r.id === diffRevB)
|
||
? revisionLabel(revisions.find((r) => r.id === diffRevB)!)
|
||
: diffRevB
|
||
: 'Ревизия B'}
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="">Ревизия B</SelectItem>
|
||
{#each revisions as rev (rev.id)}
|
||
<SelectItem value={rev.id}>{revisionLabel(rev)}</SelectItem>
|
||
{/each}
|
||
</SelectContent>
|
||
</Select>
|
||
<Button
|
||
size="sm"
|
||
class="shrink-0 self-start sm:self-auto"
|
||
onclick={onLoadDiff}
|
||
disabled={diffLoading}
|
||
>
|
||
{diffLoading ? 'Загрузка…' : 'Сравнить'}
|
||
</Button>
|
||
</div>
|
||
|
||
{#if diffData}
|
||
<div
|
||
class="grid min-h-0 grid-cols-1 overflow-hidden rounded-md border border-border sm:grid-cols-2"
|
||
style="scrollbar-gutter: stable;"
|
||
>
|
||
<div class="flex min-h-0 min-w-0 flex-col border-b border-border sm:border-r sm:border-b-0">
|
||
<div
|
||
class="flex shrink-0 items-center border-b border-border bg-muted/60 px-3 py-2 font-mono text-xs font-semibold text-success"
|
||
>
|
||
<span class="mr-2 w-10 shrink-0 text-right text-muted-foreground select-none">+</span>
|
||
<span>Добавлено ({addedSorted.length})</span>
|
||
</div>
|
||
<div
|
||
bind:this={scrollAddedEl}
|
||
class="max-h-[min(28rem,70vh)] min-h-0 overflow-x-auto overflow-y-scroll overscroll-contain bg-background/50"
|
||
onscroll={syncFromAdded}
|
||
>
|
||
{#if addedSorted.length === 0}
|
||
<p class="p-3 font-mono text-xs text-muted-foreground">Нет изменений</p>
|
||
{:else}
|
||
<table class="w-full border-collapse font-mono text-xs">
|
||
<tbody>
|
||
{#each addedSorted as line, i (`a-${i}-${line}`)}
|
||
<tr
|
||
class="border-b border-l-2 border-success/30 bg-success/5 hover:bg-muted/30"
|
||
>
|
||
<td
|
||
class="w-10 shrink-0 border-r border-transparent py-0.5 pr-1 pl-2 text-right align-top text-[11px] text-muted-foreground tabular-nums select-none"
|
||
>
|
||
{i + 1}
|
||
</td>
|
||
<td
|
||
class="max-w-0 py-0.5 pr-3 pl-1 break-all whitespace-pre-wrap text-foreground"
|
||
>
|
||
{line}
|
||
</td>
|
||
</tr>
|
||
{/each}
|
||
</tbody>
|
||
</table>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex min-h-0 min-w-0 flex-col">
|
||
<div
|
||
class="flex shrink-0 items-center border-b border-border bg-muted/60 px-3 py-2 font-mono text-xs font-semibold text-destructive"
|
||
>
|
||
<span class="mr-2 w-10 shrink-0 text-right text-muted-foreground select-none">−</span>
|
||
<span>Удалено ({removedSorted.length})</span>
|
||
</div>
|
||
<div
|
||
bind:this={scrollRemovedEl}
|
||
class="max-h-[min(28rem,70vh)] min-h-0 overflow-x-auto overflow-y-scroll overscroll-contain bg-background/50"
|
||
onscroll={syncFromRemoved}
|
||
>
|
||
{#if removedSorted.length === 0}
|
||
<p class="p-3 font-mono text-xs text-muted-foreground">Нет изменений</p>
|
||
{:else}
|
||
<table class="w-full border-collapse font-mono text-xs">
|
||
<tbody>
|
||
{#each removedSorted as line, i (`r-${i}-${line}`)}
|
||
<tr
|
||
class="border-b border-l-2 border-destructive/30 bg-destructive/5 hover:bg-muted/30"
|
||
>
|
||
<td
|
||
class="w-10 shrink-0 border-r border-transparent py-0.5 pr-1 pl-2 text-right align-top text-[11px] text-muted-foreground tabular-nums select-none"
|
||
>
|
||
{i + 1}
|
||
</td>
|
||
<td
|
||
class="max-w-0 py-0.5 pr-3 pl-1 break-all whitespace-pre-wrap text-foreground"
|
||
>
|
||
{line}
|
||
</td>
|
||
</tr>
|
||
{/each}
|
||
</tbody>
|
||
</table>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
</CardContent>
|
||
</Card>
|