mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-23 03:26:36 +03:00
fix player sessions list
This commit is contained in:
parent
9731a41139
commit
ef49fcbcdc
@ -67,11 +67,11 @@
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.8.1",
|
||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||
"svelte": "^3.55.1",
|
||||
"svelte": "~3.55.1",
|
||||
"svelte-check": "^3.0.1",
|
||||
"svelte-floating-ui": "^1.5.2",
|
||||
"svelte-french-toast": "^1.0.3",
|
||||
"svelte-loadable-store": "^1.0.1",
|
||||
"svelte-loadable-store": "^1.0.7",
|
||||
"svelte-outclick": "^3.5.0",
|
||||
"svelte-resize-observer": "^2.0.0",
|
||||
"tailwindcss": "^3.1.5",
|
||||
|
@ -155,7 +155,7 @@ devDependencies:
|
||||
specifier: ^0.3.0
|
||||
version: 0.3.0(prettier-plugin-svelte@2.9.0)(prettier@2.8.4)
|
||||
svelte:
|
||||
specifier: ^3.55.1
|
||||
specifier: ~3.55.1
|
||||
version: 3.55.1
|
||||
svelte-check:
|
||||
specifier: ^3.0.1
|
||||
@ -167,8 +167,8 @@ devDependencies:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3(svelte@3.55.1)
|
||||
svelte-loadable-store:
|
||||
specifier: ^1.0.1
|
||||
version: 1.0.1
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(svelte@3.55.1)
|
||||
svelte-outclick:
|
||||
specifier: ^3.5.0
|
||||
version: 3.5.0(svelte@3.55.1)
|
||||
@ -3655,10 +3655,12 @@ packages:
|
||||
svelte: 3.55.1
|
||||
dev: true
|
||||
|
||||
/svelte-loadable-store@1.0.1:
|
||||
resolution: {integrity: sha512-ZPRBNqaDF6PW4nruHWh73b9WWdmt5SMAZ6he9KsZo4XDvl1h4jECBKPVdnHCc4zEQMCig01BofH3bwu67G44SQ==}
|
||||
/svelte-loadable-store@1.0.7(svelte@3.55.1):
|
||||
resolution: {integrity: sha512-FLahOiMtD9lxVNS9bzbdxHC8+D0y7qxZjaaeybCyxc9uzS3/9EMTSDpv/FMTLwncQ4syOtwz5oQBO+9iAqm7hQ==}
|
||||
peerDependencies:
|
||||
svelte: ^3.0.0
|
||||
dependencies:
|
||||
svelte: 3.59.1
|
||||
svelte: 3.55.1
|
||||
dev: true
|
||||
|
||||
/svelte-outclick@3.5.0(svelte@3.55.1):
|
||||
@ -3738,11 +3740,6 @@ packages:
|
||||
engines: {node: '>= 8'}
|
||||
dev: true
|
||||
|
||||
/svelte@3.59.1:
|
||||
resolution: {integrity: sha512-pKj8fEBmqf6mq3/NfrB9SLtcJcUvjYSWyePlfCqN9gujLB25RitWK8PvFzlwim6hD/We35KbPlRteuA6rnPGcQ==}
|
||||
engines: {node: '>= 8'}
|
||||
dev: true
|
||||
|
||||
/symbol-tree@3.2.4:
|
||||
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||
dev: true
|
||||
|
@ -16,7 +16,9 @@ export default (params: { projectId: string; sessionId: string }) => {
|
||||
} else {
|
||||
set({
|
||||
...oldValue.value,
|
||||
[filePath]: [...oldValue.value[filePath], ...newDeltas]
|
||||
[filePath]: oldValue.value[filePath]
|
||||
? [...oldValue.value[filePath], ...newDeltas]
|
||||
: newDeltas
|
||||
});
|
||||
}
|
||||
})
|
||||
|
@ -1,110 +1,69 @@
|
||||
<script lang="ts">
|
||||
import type { LayoutData } from './$types';
|
||||
import { IconChevronLeft, IconChevronRight } from '$lib/icons';
|
||||
import { page } from '$app/stores';
|
||||
import { asyncDerived, derived } from '@square/svelte-store';
|
||||
import { derived } from 'svelte-loadable-store';
|
||||
import { format } from 'date-fns';
|
||||
import { onMount } from 'svelte';
|
||||
import { api, events, hotkeys, toasts } from '$lib';
|
||||
import { api, events, hotkeys, stores, toasts } from '$lib';
|
||||
import BookmarkModal from './BookmarkModal.svelte';
|
||||
import tinykeys from 'tinykeys';
|
||||
import { goto } from '$app/navigation';
|
||||
import { unsubscribe } from '$lib/utils';
|
||||
import SessionCard from './SessionCard.svelte';
|
||||
import SessionsList from './SessionsList.svelte';
|
||||
import SessionNavigations from './SessionNavigations.svelte';
|
||||
|
||||
export let data: LayoutData;
|
||||
const { currentFilepath, currentTimestamp } = data;
|
||||
|
||||
const dateSessions = derived([data.sessions, page], ([sessions, page]) =>
|
||||
sessions?.filter(
|
||||
(session) => format(session.meta.startTimestampMs, 'yyyy-MM-dd') === page.params.date
|
||||
)
|
||||
let sessions = stores.sessions({ projectId: $page.params.projectId });
|
||||
|
||||
let dateSessions = derived([sessions, page], ([sessions, page]) =>
|
||||
sessions
|
||||
.filter((session) => format(session.meta.startTimestampMs, 'yyyy-MM-dd') === page.params.date)
|
||||
.sort((a, b) => a.meta.startTimestampMs - b.meta.startTimestampMs)
|
||||
);
|
||||
|
||||
const fileFilter = derived(page, (page) => page.url.searchParams.get('file'));
|
||||
const projectId = derived(page, (page) => page.params.projectId);
|
||||
|
||||
const richSessions = asyncDerived(
|
||||
[dateSessions, fileFilter, projectId],
|
||||
async ([sessions, fileFilter, projectId]) => {
|
||||
return sessions
|
||||
.map((session) => ({
|
||||
...session,
|
||||
deltas: derived(api.deltas.Deltas({ projectId, sessionId: session.id }), (deltas) => {
|
||||
if (!fileFilter) return deltas;
|
||||
return Object.fromEntries(
|
||||
Object.entries(deltas).filter(([path]) => fileFilter.includes(path))
|
||||
);
|
||||
})
|
||||
}))
|
||||
.sort((a, b) => a.meta.startTimestampMs - b.meta.startTimestampMs);
|
||||
}
|
||||
let richSessions = derived([dateSessions, page], ([sessions, page]) =>
|
||||
sessions.map((session) => ({
|
||||
...session,
|
||||
deltas: derived(
|
||||
stores.deltas({ projectId: page.params.projectId, sessionId: session.id }),
|
||||
(deltas) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(deltas).filter(([path]) => {
|
||||
const filter = page.url.searchParams.get('file');
|
||||
return filter ? path === filter : true;
|
||||
})
|
||||
)
|
||||
),
|
||||
files: derived(
|
||||
stores.files({ projectId: page.params.projectId, sessionId: session.id }),
|
||||
(files) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(files).filter(([path]) => {
|
||||
const filter = page.url.searchParams.get('file');
|
||||
return filter ? path === filter : true;
|
||||
})
|
||||
)
|
||||
)
|
||||
}))
|
||||
);
|
||||
|
||||
const scrollToSession = () => {
|
||||
const sessionEl = document.getElementById('current-session');
|
||||
if (sessionEl) {
|
||||
sessionEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
};
|
||||
|
||||
const currentSession = derived(
|
||||
let currentSession = derived(
|
||||
[page, richSessions, data.currentSessionId],
|
||||
([page, sessions, currentSessionId]) =>
|
||||
sessions?.find((s) => s.id === currentSessionId) ??
|
||||
sessions?.find((s) => s.id === page.params.sessionId)
|
||||
);
|
||||
currentSession.subscribe(scrollToSession);
|
||||
|
||||
const nextSessionId = derived([page, richSessions], ([page, sessions]) => {
|
||||
if (sessions) {
|
||||
const currentIndex = sessions.findIndex((s) => s.id === page.params.sessionId);
|
||||
if (currentIndex === -1) return undefined;
|
||||
if (currentIndex < sessions.length - 1) return sessions[currentIndex + 1].id;
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
const prevSessionId = derived([page, richSessions], ([page, sessions]) => {
|
||||
if (sessions) {
|
||||
const currentIndex = sessions.findIndex((s) => s.id === page.params.sessionId);
|
||||
if (currentIndex === -1) return undefined;
|
||||
if (currentIndex > 0) return sessions[currentIndex - 1].id;
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
const removeFromSearchParams = (params: URLSearchParams, key: string) => {
|
||||
params.delete(key);
|
||||
return params;
|
||||
};
|
||||
|
||||
const getSessionURI = (sessionId: string) =>
|
||||
`/projects/${$page.params.projectId}/player/${
|
||||
$page.params.date
|
||||
}/${sessionId}?${removeFromSearchParams($page.url.searchParams, 'delta').toString()}`;
|
||||
|
||||
let bookmarkModal: BookmarkModal;
|
||||
|
||||
onMount(() =>
|
||||
unsubscribe(
|
||||
tinykeys(window, {
|
||||
'Shift+ArrowRight': () =>
|
||||
nextSessionId.load().then((sessionId) => {
|
||||
if (sessionId) goto(getSessionURI(sessionId));
|
||||
}),
|
||||
'Shift+ArrowLeft': () =>
|
||||
prevSessionId.load().then((sessionId) => {
|
||||
if (sessionId) goto(getSessionURI(sessionId));
|
||||
})
|
||||
}),
|
||||
|
||||
events.on('openBookmarkModal', () => bookmarkModal?.show($currentTimestamp)),
|
||||
hotkeys.on('Meta+Shift+D', () => bookmarkModal?.show($currentTimestamp)),
|
||||
hotkeys.on('D', () =>
|
||||
api.bookmarks
|
||||
.upsert({
|
||||
projectId: $projectId,
|
||||
projectId: $page.params.projectId,
|
||||
note: '',
|
||||
timestampMs: $currentTimestamp,
|
||||
deleted: false
|
||||
@ -116,82 +75,34 @@
|
||||
</script>
|
||||
|
||||
<article id="activities" class="card my-2 flex w-80 flex-shrink-0 flex-grow-0 flex-col xl:w-96">
|
||||
{#await richSessions.load()}
|
||||
{#if $richSessions.isLoading || $currentSession.isLoading}
|
||||
<div class="flex h-full flex-col items-center justify-center">
|
||||
<div
|
||||
class="loader border-gray-200 mb-4 h-12 w-12 rounded-full border-4 border-t-4 ease-linear"
|
||||
/>
|
||||
<h2 class="text-center text-2xl font-medium text-gray-500">Loading...</h2>
|
||||
</div>
|
||||
{:then}
|
||||
<header
|
||||
class="card-header flex flex-row justify-between rounded-t border-b-[1px] border-b-divider bg-card-active px-3 py-2 leading-[21px]"
|
||||
>
|
||||
<div class="relative flex gap-2">
|
||||
<div class="relative bottom-[1px] h-4 w-4 text-sm">🧰</div>
|
||||
<div>Working History</div>
|
||||
<div class="text-zinc-400">
|
||||
{$richSessions.length}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<ul
|
||||
class="mr-1 flex h-full flex-col gap-2 overflow-auto rounded-b bg-card-default pt-2 pb-2 pl-2 pr-1"
|
||||
>
|
||||
{#each $richSessions as session}
|
||||
{@const isCurrent = session.id === $currentSession?.id}
|
||||
<SessionCard
|
||||
{isCurrent}
|
||||
{session}
|
||||
deltas={session.deltas}
|
||||
currentFilepath={$currentFilepath}
|
||||
/>
|
||||
{:else}
|
||||
<div class="mt-4 text-center text-zinc-300">No activities found</div>
|
||||
{/each}
|
||||
</ul>
|
||||
{/await}
|
||||
{:else}
|
||||
<SessionsList
|
||||
sessions={$richSessions.value}
|
||||
currentSession={$currentSession.value}
|
||||
currentFilepath={$currentFilepath}
|
||||
/>
|
||||
{/if}
|
||||
</article>
|
||||
|
||||
<div id="player" class="card relative my-2 flex flex-auto flex-col overflow-auto">
|
||||
<header class="flex items-center gap-3 bg-card-active px-3 py-2">
|
||||
{#await Promise.all([currentSession.load(), nextSessionId.load(), prevSessionId.load()])}
|
||||
{#if $currentSession.isLoading || $richSessions.isLoading}
|
||||
<span>Loading...</span>
|
||||
{:then}
|
||||
{#if !$currentSession}
|
||||
<span>No session found</span>
|
||||
{:else}
|
||||
<span class="min-w-[200px]">
|
||||
{format($currentSession.meta.startTimestampMs, 'EEEE, LLL d, HH:mm')}
|
||||
-
|
||||
{format($currentSession.meta.lastTimestampMs, 'HH:mm')}
|
||||
</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<a
|
||||
href={$prevSessionId && getSessionURI($prevSessionId)}
|
||||
class="rounded border border-zinc-500 bg-zinc-600 p-0.5"
|
||||
class:hover:bg-zinc-500={!!$prevSessionId}
|
||||
class:pointer-events-none={!$prevSessionId}
|
||||
class:text-zinc-500={!$prevSessionId}
|
||||
>
|
||||
<IconChevronLeft class="h-4 w-4" />
|
||||
</a>
|
||||
<a
|
||||
href={$nextSessionId && getSessionURI($nextSessionId)}
|
||||
class="rounded border border-zinc-500 bg-zinc-600 p-0.5"
|
||||
class:hover:bg-zinc-500={!!$nextSessionId}
|
||||
class:pointer-events-none={!$nextSessionId}
|
||||
class:text-zinc-500={!$nextSessionId}
|
||||
>
|
||||
<IconChevronRight class="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
{:else if !$currentSession.value}
|
||||
<span>No session found</span>
|
||||
{:else}
|
||||
<SessionNavigations currentSession={$currentSession.value} sessions={$richSessions.value} />
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<BookmarkModal bind:this={bookmarkModal} projectId={$projectId} />
|
||||
<BookmarkModal bind:this={bookmarkModal} projectId={$page.params.projectId} />
|
||||
|
@ -2,34 +2,31 @@
|
||||
import type { Delta, Session } from '$lib/api';
|
||||
import { page } from '$app/stores';
|
||||
import { collapse } from '$lib/paths';
|
||||
import { derived, type Loadable } from '@square/svelte-store';
|
||||
import { derived } from '@square/svelte-store';
|
||||
import { stores } from '$lib';
|
||||
import { IconBookmarkFilled } from '$lib/icons';
|
||||
|
||||
export let isCurrent: boolean;
|
||||
export let session: Session;
|
||||
export let currentFilepath: string;
|
||||
export let deltas: Loadable<Record<string, Delta[]>>;
|
||||
export let deltas: Record<string, Delta[]>;
|
||||
|
||||
$: bookmarks = derived(
|
||||
[stores.bookmarks({ projectId: session.projectId }), deltas],
|
||||
([bookmarks, deltas]) => {
|
||||
if (bookmarks.isLoading) return [];
|
||||
const timestamps = Object.values(deltas ?? {}).flatMap((deltas) =>
|
||||
deltas.map((d) => d.timestampMs)
|
||||
);
|
||||
const start = Math.min(...timestamps);
|
||||
const end = Math.max(...timestamps);
|
||||
return bookmarks.value
|
||||
.filter((bookmark) => !bookmark.deleted)
|
||||
.filter((bookmark) => bookmark.timestampMs >= start && bookmark.timestampMs < end);
|
||||
}
|
||||
);
|
||||
$: bookmarks = derived(stores.bookmarks({ projectId: session.projectId }), (bookmarks) => {
|
||||
if (bookmarks.isLoading) return [];
|
||||
const timestamps = Object.values(deltas ?? {}).flatMap((deltas) =>
|
||||
deltas.map((d) => d.timestampMs)
|
||||
);
|
||||
const start = Math.min(...timestamps);
|
||||
const end = Math.max(...timestamps);
|
||||
return bookmarks.value
|
||||
.filter((bookmark) => !bookmark.deleted)
|
||||
.filter((bookmark) => bookmark.timestampMs >= start && bookmark.timestampMs < end);
|
||||
});
|
||||
|
||||
const unique = (value: any, index: number, self: any[]) => self.indexOf(value) === index;
|
||||
const lexically = (a: string, b: string) => a.localeCompare(b);
|
||||
|
||||
const changedFiles = derived(deltas, (deltas) => Object.keys(deltas ?? {}).filter(unique));
|
||||
const changedFiles = Object.keys(deltas ?? {}).filter(unique);
|
||||
|
||||
const sessionDuration = (session: Session) =>
|
||||
`${Math.round((session.meta.lastTimestampMs - session.meta.startTimestampMs) / 1000 / 60)} min`;
|
||||
@ -59,9 +56,15 @@
|
||||
`/projects/${$page.params.projectId}/player/${
|
||||
$page.params.date
|
||||
}/${sessionId}?${removeFromSearchParams($page.url.searchParams, 'delta').toString()}`;
|
||||
|
||||
let card: HTMLLIElement;
|
||||
$: if (isCurrent) {
|
||||
card?.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<li
|
||||
bind:this={card}
|
||||
id={isCurrent ? 'current-session' : ''}
|
||||
class:bg-card-active={isCurrent}
|
||||
class="session-card relative rounded border-[0.5px] border-gb-700 text-zinc-300 shadow-md transition-colors duration-200 ease-in-out hover:bg-card-active"
|
||||
@ -81,27 +84,25 @@
|
||||
</div>
|
||||
|
||||
<span class="flex flex-row justify-between px-3 pb-3">
|
||||
{$changedFiles.length}
|
||||
{$changedFiles.length !== 1 ? 'files' : 'file'}
|
||||
{changedFiles.length}
|
||||
{changedFiles.length !== 1 ? 'files' : 'file'}
|
||||
</span>
|
||||
|
||||
{#if isCurrent}
|
||||
{#await changedFiles.load() then}
|
||||
<ul
|
||||
class="list-disk list-none overflow-hidden rounded-bl rounded-br bg-zinc-800 py-1 pl-0 pr-2"
|
||||
style:list-style="disc"
|
||||
>
|
||||
{#each $changedFiles.sort(lexically) as filename}
|
||||
<li
|
||||
class:text-zinc-100={currentFilepath === filename}
|
||||
class:bg-[#3356C2]={currentFilepath === filename}
|
||||
class="mx-5 ml-1 w-full list-none rounded p-1 text-zinc-500"
|
||||
>
|
||||
{collapse(filename)}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/await}
|
||||
<ul
|
||||
class="list-disk list-none overflow-hidden rounded-bl rounded-br bg-zinc-800 py-1 pl-0 pr-2"
|
||||
style:list-style="disc"
|
||||
>
|
||||
{#each changedFiles.sort(lexically) as filename}
|
||||
<li
|
||||
class:text-zinc-100={currentFilepath === filename}
|
||||
class:bg-[#3356C2]={currentFilepath === filename}
|
||||
class="mx-5 ml-1 w-full list-none rounded p-1 text-zinc-500"
|
||||
>
|
||||
{collapse(filename)}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</a>
|
||||
</li>
|
||||
|
@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { IconChevronLeft, IconChevronRight } from '$lib/icons';
|
||||
import { page } from '$app/stores';
|
||||
import { hotkeys } from '$lib';
|
||||
|
||||
import type { Delta, Session } from '$lib/api';
|
||||
import { unsubscribe } from '$lib/utils';
|
||||
import type { Readable } from '@square/svelte-store';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Loadable } from 'svelte-loadable-store';
|
||||
import { derived } from 'svelte-loadable-store';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
export let sessions: (Session & {
|
||||
deltas: Readable<Loadable<Record<string, Delta[]>>>;
|
||||
})[];
|
||||
export let currentSession: Session;
|
||||
|
||||
$: sessionDeltas = derived(
|
||||
sessions.map(({ deltas }) => deltas),
|
||||
(deltas) => deltas
|
||||
);
|
||||
|
||||
$: nextSessionId = derived([page, sessionDeltas], ([page, sessionDeltas]) => {
|
||||
if (sessions) {
|
||||
const currentIndex = sessions.findIndex((s) => s.id === page.params.sessionId);
|
||||
if (currentIndex === -1) return undefined;
|
||||
for (let i = currentIndex + 1; i < sessions.length; i++) {
|
||||
if (Object.keys(sessionDeltas[i]).length > 0) return sessions[i].id;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
$: prevSessionId = derived([page, sessionDeltas], ([page, sessionDeltas]) => {
|
||||
if (sessions) {
|
||||
const currentIndex = sessions.findIndex((s) => s.id === page.params.sessionId);
|
||||
if (currentIndex === -1) return undefined;
|
||||
for (let i = currentIndex - 1; i >= 0; i--) {
|
||||
if (Object.keys(sessionDeltas[i]).length > 0) return sessions[i].id;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
const removeFromSearchParams = (params: URLSearchParams, key: string) => {
|
||||
params.delete(key);
|
||||
return params;
|
||||
};
|
||||
|
||||
const getSessionURI = (sessionId: string) =>
|
||||
`/projects/${$page.params.projectId}/player/${
|
||||
$page.params.date
|
||||
}/${sessionId}?${removeFromSearchParams($page.url.searchParams, 'delta').toString()}`;
|
||||
|
||||
onMount(() =>
|
||||
unsubscribe(
|
||||
hotkeys.on('Shift+ArrowRight', () => {
|
||||
if (!$nextSessionId.isLoading && $nextSessionId.value)
|
||||
goto(getSessionURI($nextSessionId.value));
|
||||
}),
|
||||
hotkeys.on('Shift+ArrowLeft', () => {
|
||||
if (!$prevSessionId.isLoading && $prevSessionId.value)
|
||||
goto(getSessionURI($prevSessionId.value));
|
||||
})
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<span class="min-w-[200px]">
|
||||
{format(currentSession.meta.startTimestampMs, 'EEEE, LLL d, HH:mm')}
|
||||
-
|
||||
{format(currentSession.meta.lastTimestampMs, 'HH:mm')}
|
||||
</span>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
{#if !$prevSessionId.isLoading && !$nextSessionId.isLoading}
|
||||
<a
|
||||
href={$prevSessionId.value && getSessionURI($prevSessionId.value)}
|
||||
class="rounded border border-zinc-500 bg-zinc-600 p-0.5"
|
||||
class:hover:bg-zinc-500={!!$prevSessionId.value}
|
||||
class:pointer-events-none={!$prevSessionId.value}
|
||||
class:text-zinc-500={!$prevSessionId.value}
|
||||
>
|
||||
<IconChevronLeft class="h-4 w-4" />
|
||||
</a>
|
||||
<a
|
||||
href={$nextSessionId.value && getSessionURI($nextSessionId.value)}
|
||||
class="rounded border border-zinc-500 bg-zinc-600 p-0.5"
|
||||
class:hover:bg-zinc-500={!!$nextSessionId.value}
|
||||
class:pointer-events-none={!$nextSessionId.value}
|
||||
class:text-zinc-500={!$nextSessionId.value}
|
||||
>
|
||||
<IconChevronRight class="h-4 w-4" />
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import type { Delta, Session } from '$lib/api';
|
||||
import type { Readable } from '@square/svelte-store';
|
||||
import type { Loadable } from 'svelte-loadable-store';
|
||||
import { derived } from 'svelte-loadable-store';
|
||||
import SessionCard from './SessionCard.svelte';
|
||||
|
||||
export let sessions: (Session & {
|
||||
deltas: Readable<Loadable<Record<string, Delta[]>>>;
|
||||
files: Readable<Loadable<Record<string, string>>>;
|
||||
})[];
|
||||
export let currentSession: Session | undefined;
|
||||
export let currentFilepath: string;
|
||||
|
||||
$: visibleDeltas = derived(
|
||||
sessions.map(({ deltas }) => deltas),
|
||||
(deltas) => deltas.map((delta) => Object.fromEntries(Object.entries(delta ?? {})))
|
||||
);
|
||||
|
||||
$: visibleSessions = sessions?.map((session, i) => ({
|
||||
...session,
|
||||
visible: !$visibleDeltas.isLoading && Object.keys($visibleDeltas.value[i]).length > 0
|
||||
}));
|
||||
</script>
|
||||
|
||||
<header
|
||||
class="card-header flex flex-row justify-between rounded-t border-b-[1px] border-b-divider bg-card-active px-3 py-2 leading-[21px]"
|
||||
>
|
||||
<div class="relative flex gap-2">
|
||||
<div class="relative bottom-[1px] h-4 w-4 text-sm">🧰</div>
|
||||
<div>Working History</div>
|
||||
<div class="text-zinc-400">
|
||||
{visibleSessions.filter(({ visible }) => visible).length}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<ul
|
||||
class="mr-1 flex h-full flex-col gap-2 overflow-auto rounded-b bg-card-default pt-2 pb-2 pl-2 pr-1"
|
||||
>
|
||||
{#each visibleSessions as session, i}
|
||||
{@const isCurrent = session.id === currentSession?.id}
|
||||
{#if session.visible && !$visibleDeltas.isLoading}
|
||||
<SessionCard {isCurrent} {session} deltas={$visibleDeltas.value[i]} {currentFilepath} />
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="mt-4 text-center text-zinc-300">No activities found</div>
|
||||
{/each}
|
||||
</ul>
|
Loading…
Reference in New Issue
Block a user