mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-23 20:54:50 +03:00
Listing series
This commit is contained in:
parent
1345c324db
commit
2c00d5f869
@ -4,7 +4,7 @@ import * as toasts from '$lib/utils/toasts';
|
||||
import { persisted } from '@gitbutler/shared/persisted';
|
||||
import { open } from '@tauri-apps/api/dialog';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { derived, get, writable, type Readable } from 'svelte/store';
|
||||
import type { HttpClient } from '@gitbutler/shared/httpClient';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
@ -81,6 +81,18 @@ export class ProjectService {
|
||||
return plainToInstance(Project, await invoke('get_project', { id: projectId, noValidation }));
|
||||
}
|
||||
|
||||
#projectStores = new Map<string, Readable<Project | undefined>>();
|
||||
getProjectStore(projectId: string) {
|
||||
let store = this.#projectStores.get(projectId);
|
||||
if (store) return store;
|
||||
|
||||
store = derived(this.projects, (projects) => {
|
||||
return projects.find((p) => p.id === projectId);
|
||||
});
|
||||
this.#projectStores.set(projectId, store);
|
||||
return store;
|
||||
}
|
||||
|
||||
async updateProject(project: Project) {
|
||||
plainToInstance(Project, await invoke('update_project', { project: project }));
|
||||
this.reload();
|
||||
|
34
apps/desktop/src/lib/navigation/CloudSeriesButton.svelte
Normal file
34
apps/desktop/src/lib/navigation/CloudSeriesButton.svelte
Normal file
@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import DomainButton from '$lib/navigation/DomainButton.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
interface Props {
|
||||
href: string;
|
||||
isNavCollapsed: boolean;
|
||||
}
|
||||
|
||||
const { href, isNavCollapsed }: Props = $props();
|
||||
const label = 'Series';
|
||||
</script>
|
||||
|
||||
<DomainButton
|
||||
isSelected={$page.url.pathname === href}
|
||||
{isNavCollapsed}
|
||||
tooltipLabel={label}
|
||||
onmousedown={async () => await goto(href)}
|
||||
>
|
||||
<img class="icon" src="/images/domain-icons/working-branches.svg" alt="" />
|
||||
{#if !isNavCollapsed}
|
||||
<span class="text-14 text-semibold" class:collapsed-txt={isNavCollapsed}>{label}</span>
|
||||
{/if}
|
||||
</DomainButton>
|
||||
|
||||
<style lang="postcss">
|
||||
.icon {
|
||||
border-radius: var(--radius-s);
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
@ -5,7 +5,7 @@
|
||||
import TargetCard from './TargetCard.svelte';
|
||||
import WorkspaceButton from './WorkspaceButton.svelte';
|
||||
import Resizer from '../shared/Resizer.svelte';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { Project, ProjectService } from '$lib/backend/projects';
|
||||
import { featureTopics } from '$lib/config/uiFeatureFlags';
|
||||
import { ModeService } from '$lib/modes/service';
|
||||
import EditButton from '$lib/navigation/EditButton.svelte';
|
||||
@ -16,11 +16,14 @@
|
||||
import { getContext, getContextStoreBySymbol } from '@gitbutler/shared/context';
|
||||
import { persisted } from '@gitbutler/shared/persisted';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import CloudSeriesButton from '$lib/navigation/CloudSeriesButton.svelte';
|
||||
|
||||
const minResizerWidth = 280;
|
||||
const minResizerRatio = 150;
|
||||
const userSettings = getContextStoreBySymbol<Settings>(SETTINGS);
|
||||
const project = getContext(Project);
|
||||
const projectService = getContext(ProjectService);
|
||||
const project$ = projectService.getProjectStore(project.id);
|
||||
const defaultTrayWidthRem = persisted<number | undefined>(
|
||||
undefined,
|
||||
'defaulTrayWidth_ ' + project.id
|
||||
@ -126,6 +129,10 @@
|
||||
{#if $topicsEnabled}
|
||||
<TopicsButton href={`/${project.id}/topics`} isNavCollapsed={$isNavCollapsed} />
|
||||
{/if}
|
||||
|
||||
{#if $project$?.api?.sync}
|
||||
<CloudSeriesButton href={`/${$project$.id}/series`} isNavCollapsed={$isNavCollapsed} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Project, ProjectService } from '$lib/backend/projects';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import AiPromptSelect from '$lib/components/AIPromptSelect.svelte';
|
||||
import SectionCard from '$lib/components/SectionCard.svelte';
|
||||
import WelcomeSigninAction from '$lib/components/WelcomeSigninAction.svelte';
|
||||
@ -10,24 +10,13 @@
|
||||
import { UserService } from '$lib/stores/user';
|
||||
import { getContext } from '@gitbutler/shared/context';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
const userService = getContext(UserService);
|
||||
const projectService = getContext(ProjectService);
|
||||
const project = getContext(Project);
|
||||
const user = userService.user;
|
||||
|
||||
const aiGenEnabled = projectAiGenEnabled(project.id);
|
||||
|
||||
onMount(async () => {
|
||||
if (!project?.api) return;
|
||||
if (!$user) return;
|
||||
const cloudProject = await projectService.getCloudProject(project.api.repository_id);
|
||||
if (cloudProject === project.api) return;
|
||||
project.api = { ...cloudProject, sync: project.api.sync, sync_code: project.api.sync_code };
|
||||
projectService.updateProject(project);
|
||||
});
|
||||
</script>
|
||||
|
||||
<Section>
|
||||
|
@ -10,6 +10,7 @@
|
||||
import { User } from '$lib/stores/user';
|
||||
import * as toasts from '$lib/utils/toasts';
|
||||
import { getContext, getContextStore } from '@gitbutler/shared/context';
|
||||
import { onMount } from 'svelte';
|
||||
import { PUBLIC_API_BASE_URL } from '$env/static/public';
|
||||
|
||||
const project = getContext(Project);
|
||||
@ -19,18 +20,6 @@
|
||||
let title = project?.title;
|
||||
let description = project?.description;
|
||||
|
||||
async function saveProject() {
|
||||
const api =
|
||||
$user && project.api
|
||||
? await projectService.updateCloudProject(project.api.repository_id, {
|
||||
name: project.title,
|
||||
description: project.description
|
||||
})
|
||||
: undefined;
|
||||
project.api = api ? { ...api, sync: false, sync_code: undefined } : undefined;
|
||||
projectService.updateProject(project);
|
||||
}
|
||||
|
||||
async function onSyncChange(sync: boolean) {
|
||||
if (!$user) return;
|
||||
try {
|
||||
@ -66,6 +55,16 @@
|
||||
toasts.error('Failed to update project sync status');
|
||||
}
|
||||
}
|
||||
|
||||
// This is some janky bullshit, but it works well enough for now
|
||||
onMount(async () => {
|
||||
if (!project?.api) return;
|
||||
if (!$user) return;
|
||||
console.log(project);
|
||||
const cloudProject = await projectService.getCloudProject(project.api.repository_id);
|
||||
project.api = { ...cloudProject, sync: project.api.sync, sync_code: project.api.sync_code };
|
||||
projectService.updateProject(project);
|
||||
});
|
||||
</script>
|
||||
|
||||
<SectionCard>
|
||||
@ -81,7 +80,7 @@
|
||||
required
|
||||
on:change={(e) => {
|
||||
project.title = e.detail;
|
||||
saveProject();
|
||||
projectService.updateProject(project);
|
||||
}}
|
||||
/>
|
||||
<TextArea
|
||||
@ -91,7 +90,7 @@
|
||||
bind:value={description}
|
||||
on:change={() => {
|
||||
project.description = description;
|
||||
saveProject();
|
||||
projectService.updateProject(project);
|
||||
}}
|
||||
maxHeight={300}
|
||||
/>
|
||||
@ -114,7 +113,7 @@
|
||||
<Toggle
|
||||
id="historySync"
|
||||
checked={project.api?.sync || false}
|
||||
on:click={async (e) => await onSyncChange(!!e.detail)}
|
||||
on:click={async () => await onSyncChange(!project.api?.sync)}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
@ -126,7 +125,7 @@
|
||||
<Toggle
|
||||
id="branchesySync"
|
||||
checked={project.api?.sync_code || false}
|
||||
on:click={async (e) => await onSyncCodeChange(!!e.detail)}
|
||||
on:click={async () => await onSyncCodeChange(!project.api?.sync_code)}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
|
@ -33,6 +33,7 @@
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { UpstreamIntegrationService } from '$lib/vbranches/upstreamIntegrationService';
|
||||
import { VirtualBranchService } from '$lib/vbranches/virtualBranch';
|
||||
import { CloudPatchStacksService } from '@gitbutler/shared/cloud/stacks/service';
|
||||
import { onDestroy, setContext, type Snippet } from 'svelte';
|
||||
import { derived as storeDerived } from 'svelte/store';
|
||||
import type { LayoutData } from './$types';
|
||||
@ -78,6 +79,7 @@
|
||||
setContext(ModeService, data.modeService);
|
||||
setContext(UncommitedFilesWatcher, data.uncommitedFileWatcher);
|
||||
setContext(UpstreamIntegrationService, data.upstreamIntegrationService);
|
||||
setContext(CloudPatchStacksService, data.cloudPatchStacksService);
|
||||
});
|
||||
|
||||
let intervalId: any;
|
||||
|
@ -14,7 +14,12 @@ import { UncommitedFilesWatcher } from '$lib/uncommitedFiles/watcher';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { UpstreamIntegrationService } from '$lib/vbranches/upstreamIntegrationService';
|
||||
import { VirtualBranchService } from '$lib/vbranches/virtualBranch';
|
||||
import {
|
||||
PatchStacksApiService,
|
||||
CloudPatchStacksService
|
||||
} from '@gitbutler/shared/cloud/stacks/service';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { derived } from 'svelte/store';
|
||||
import type { Project } from '$lib/backend/projects';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
@ -22,11 +27,7 @@ export const prerender = false;
|
||||
|
||||
// eslint-disable-next-line
|
||||
export const load: LayoutLoad = async ({ params, parent }) => {
|
||||
// prettier-ignore
|
||||
const {
|
||||
authService,
|
||||
projectService,
|
||||
} = await parent();
|
||||
const { authService, projectService, cloud } = await parent();
|
||||
|
||||
const projectId = params.projectId;
|
||||
projectService.setLastOpenedProject(projectId);
|
||||
@ -84,6 +85,11 @@ export const load: LayoutLoad = async ({ params, parent }) => {
|
||||
|
||||
const uncommitedFileWatcher = new UncommitedFilesWatcher(project);
|
||||
const upstreamIntegrationService = new UpstreamIntegrationService(project, vbranchService);
|
||||
const repositoryId = derived(projectService.getProjectStore(projectId), (project) => {
|
||||
return project?.api?.repository_id;
|
||||
});
|
||||
const patchStacksApiService = new PatchStacksApiService(cloud);
|
||||
const cloudPatchStacksService = new CloudPatchStacksService(repositoryId, patchStacksApiService);
|
||||
|
||||
return {
|
||||
authService,
|
||||
@ -99,6 +105,7 @@ export const load: LayoutLoad = async ({ params, parent }) => {
|
||||
modeService,
|
||||
fetchSignal,
|
||||
upstreamIntegrationService,
|
||||
cloudPatchStacksService,
|
||||
|
||||
// These observables are provided for convenience
|
||||
branchDragActionsFactory,
|
||||
|
5
apps/desktop/src/routes/[projectId]/series/+page.svelte
Normal file
5
apps/desktop/src/routes/[projectId]/series/+page.svelte
Normal file
@ -0,0 +1,5 @@
|
||||
<script lang="ts">
|
||||
import CloudPatchStackIndex from '@gitbutler/shared/cloud/stacks/CloudPatchStackIndex.svelte';
|
||||
</script>
|
||||
|
||||
<CloudPatchStackIndex />
|
@ -7,7 +7,7 @@ use anyhow::{anyhow, Context, Result};
|
||||
use gitbutler_command_context::CommandContext;
|
||||
use gitbutler_error::error::Code;
|
||||
use gitbutler_id::id::Id;
|
||||
use gitbutler_oplog::OplogExt;
|
||||
use gitbutler_oplog::{/*entry::SnapshotDetails,*/ OplogExt};
|
||||
use gitbutler_project as projects;
|
||||
use gitbutler_project::{CodePushState, Project};
|
||||
use gitbutler_reference::Refname;
|
||||
@ -16,6 +16,11 @@ use gitbutler_url::Url;
|
||||
use gitbutler_user as users;
|
||||
use itertools::Itertools;
|
||||
|
||||
// pub fn take_synced_snapshot(project: &Project, user: &users::User) -> Result<git2::Oid> {
|
||||
// let command_context = CommandContext::open(project)?;
|
||||
// project.create_snapshot(SnapshotDetails::new(), perm)
|
||||
// }
|
||||
|
||||
/// Pushes the repository to the GitButler remote
|
||||
pub fn push_repo(
|
||||
ctx: &CommandContext,
|
||||
|
@ -5,6 +5,7 @@ use gitbutler_diff::FileDiff;
|
||||
use gitbutler_oplog::{entry::Snapshot, OplogExt};
|
||||
use gitbutler_project as projects;
|
||||
use gitbutler_project::ProjectId;
|
||||
// use gitbutler_user::User;
|
||||
use tauri::State;
|
||||
use tracing::instrument;
|
||||
|
||||
@ -50,3 +51,15 @@ pub fn snapshot_diff(
|
||||
let diff = project.snapshot_diff(sha.parse().map_err(anyhow::Error::from)?)?;
|
||||
Ok(diff)
|
||||
}
|
||||
|
||||
// #[tauri::command(async)]
|
||||
// #[instrument(skip(projects), err(Debug))]
|
||||
// pub fn take_synced_snapshot(
|
||||
// projects: State<'_, projects::Controller>,
|
||||
// project_id: ProjectId,
|
||||
// user: User,
|
||||
// ) -> Result<git2::Oid, Error> {
|
||||
// let project = projects.get(project_id).context("failed to get project")?;
|
||||
// let snapshot_oid = gitbutler_sync::cloud::take_synced_snapshot(&project, &user)?;
|
||||
// Ok(snapshot_oid)
|
||||
// }
|
||||
|
@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { CloudPatchStacksService } from '$lib/cloud/stacks/service';
|
||||
import { getContext } from '$lib/context';
|
||||
|
||||
/**
|
||||
* Expects the following contexts:
|
||||
* - PatchStacksService
|
||||
*/
|
||||
|
||||
const patchStacksService = getContext(CloudPatchStacksService);
|
||||
const patchStacks = $derived(patchStacksService.patchStacks);
|
||||
</script>
|
||||
|
||||
{#each $patchStacks as patchStack}
|
||||
<div>{patchStack.title}</div>
|
||||
{/each}
|
@ -1,4 +1,5 @@
|
||||
import { derived, writable, type Readable } from 'svelte/store';
|
||||
import { writableDerived } from '$lib/storeUtils';
|
||||
import { derived, get, type Readable, type Writable } from 'svelte/store';
|
||||
import type { HttpClient } from '$lib/httpClient';
|
||||
|
||||
interface ApiPatchStatstics {
|
||||
@ -9,12 +10,12 @@ interface ApiPatchStatstics {
|
||||
files: string[];
|
||||
}
|
||||
|
||||
class PatchStatstics {
|
||||
fileCount: number;
|
||||
sectionCount: number;
|
||||
lines: number;
|
||||
deletions: number;
|
||||
files: string[];
|
||||
export class CloudPatchStatsitics {
|
||||
readonly fileCount: number;
|
||||
readonly sectionCount: number;
|
||||
readonly lines: number;
|
||||
readonly deletions: number;
|
||||
readonly files: string[];
|
||||
|
||||
constructor(apiPatchStatstics: ApiPatchStatstics) {
|
||||
this.fileCount = apiPatchStatstics.file_count;
|
||||
@ -31,10 +32,10 @@ interface ApiPatchReview {
|
||||
rejected: boolean;
|
||||
}
|
||||
|
||||
class PatchReview {
|
||||
viewed: boolean;
|
||||
signedOff: boolean;
|
||||
rejected: boolean;
|
||||
export class CloudPatchReview {
|
||||
readonly viewed: boolean;
|
||||
readonly signedOff: boolean;
|
||||
readonly rejected: boolean;
|
||||
|
||||
constructor(apiPatchReview: ApiPatchReview) {
|
||||
this.viewed = apiPatchReview.viewed;
|
||||
@ -57,8 +58,7 @@ interface ApiPatch {
|
||||
review_all: ApiPatchReview[];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
class Patch {
|
||||
export class CloudPatch {
|
||||
changeId: string;
|
||||
commitSha: string;
|
||||
title?: string;
|
||||
@ -66,9 +66,9 @@ class Patch {
|
||||
position: number;
|
||||
version: number;
|
||||
contributors: string[];
|
||||
statistics: PatchStatstics;
|
||||
review: PatchReview;
|
||||
reviewAll: PatchReview[];
|
||||
statistics: CloudPatchStatsitics;
|
||||
review: CloudPatchReview;
|
||||
reviewAll: CloudPatchReview[];
|
||||
|
||||
constructor(apiPatch: ApiPatch) {
|
||||
this.changeId = apiPatch.change_id;
|
||||
@ -78,13 +78,13 @@ class Patch {
|
||||
this.position = apiPatch.position || 0;
|
||||
this.version = apiPatch.version || 0;
|
||||
this.contributors = apiPatch.contributors;
|
||||
this.statistics = new PatchStatstics(apiPatch.statistics);
|
||||
this.review = new PatchReview(apiPatch.review);
|
||||
this.reviewAll = apiPatch.review_all.map((review) => new PatchReview(review));
|
||||
this.statistics = new CloudPatchStatsitics(apiPatch.statistics);
|
||||
this.review = new CloudPatchReview(apiPatch.review);
|
||||
this.reviewAll = apiPatch.review_all.map((review) => new CloudPatchReview(review));
|
||||
}
|
||||
}
|
||||
|
||||
const enum PatchStackStatus {
|
||||
export const enum CloudPatchStackStatus {
|
||||
Active = 'active',
|
||||
Inactive = 'inactive',
|
||||
Closed = 'closed',
|
||||
@ -98,7 +98,7 @@ interface ApiPatchStack {
|
||||
uuid: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
status?: PatchStackStatus;
|
||||
status?: CloudPatchStackStatus;
|
||||
version?: number;
|
||||
created_at: string;
|
||||
stack_size?: number;
|
||||
@ -106,13 +106,13 @@ interface ApiPatchStack {
|
||||
patches: ApiPatch[];
|
||||
}
|
||||
|
||||
class PatchStack {
|
||||
export class CloudPatchStack {
|
||||
branchId: string;
|
||||
oplogSha?: string;
|
||||
uuid: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
status?: PatchStackStatus;
|
||||
status?: CloudPatchStackStatus;
|
||||
version: number;
|
||||
createdAt: string;
|
||||
stackSize: number;
|
||||
@ -136,32 +136,35 @@ class PatchStack {
|
||||
}
|
||||
}
|
||||
|
||||
interface PatchStackCreationParams {
|
||||
export interface PatchStackCreationParams {
|
||||
branch_id: string;
|
||||
oplog_sha: string;
|
||||
}
|
||||
|
||||
interface PatchStackUpdateParams {
|
||||
export interface PatchStackUpdateParams {
|
||||
status: 'active' | 'closed';
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export class PatchStacksApiService {
|
||||
constructor(
|
||||
private readonly repositoryId: string,
|
||||
private readonly httpClient: HttpClient
|
||||
) {}
|
||||
constructor(private readonly httpClient: HttpClient) {}
|
||||
|
||||
async getPatchStacks(status: PatchStackStatus = PatchStackStatus.All): Promise<ApiPatchStack[]> {
|
||||
// TODO(CTO): Support optional filtering query params `branch_id` and `status`
|
||||
async getPatchStacks(
|
||||
repositoryId: string,
|
||||
status: CloudPatchStackStatus = CloudPatchStackStatus.All
|
||||
): Promise<ApiPatchStack[]> {
|
||||
// TODO(CTO): Support optional filtering query param `branch_id`
|
||||
return await this.httpClient.get<ApiPatchStack[]>(
|
||||
`/patch_stack/${this.repositoryId}?status=${status}`
|
||||
`patch_stack/${repositoryId}?status=${status}`
|
||||
);
|
||||
}
|
||||
|
||||
async createPatchStack(params: PatchStackCreationParams): Promise<ApiPatchStack> {
|
||||
return await this.httpClient.post<ApiPatchStack>(`/patch_stack/${this.repositoryId}`, {
|
||||
async createPatchStack(
|
||||
repositoryId: string,
|
||||
params: PatchStackCreationParams
|
||||
): Promise<ApiPatchStack> {
|
||||
return await this.httpClient.post<ApiPatchStack>(`patch_stack/${repositoryId}`, {
|
||||
body: params
|
||||
});
|
||||
}
|
||||
@ -170,7 +173,7 @@ export class PatchStacksApiService {
|
||||
patchStackUuid: string,
|
||||
params: PatchStackUpdateParams
|
||||
): Promise<ApiPatchStack> {
|
||||
return await this.httpClient.put<ApiPatchStack>(`/patch_stack/${patchStackUuid}`, {
|
||||
return await this.httpClient.put<ApiPatchStack>(`patch_stack/${patchStackUuid}`, {
|
||||
body: params
|
||||
});
|
||||
}
|
||||
@ -184,43 +187,60 @@ const MINUTES_15 = 15 * 60 * 1000;
|
||||
* The list of patch stacks is kept up-to-date automatically, whenever
|
||||
* operations on a patch stack have been performed, or every 15 minutes.
|
||||
*/
|
||||
export class PatchStacksService {
|
||||
#apiPatchStacks = writable<ApiPatchStack[]>([], (set) => {
|
||||
let canceled = false;
|
||||
|
||||
const callback = (() => {
|
||||
this.patchStacksApiService.getPatchStacks().then((patchStacks) => {
|
||||
if (!canceled) set(patchStacks);
|
||||
});
|
||||
}).bind(this);
|
||||
|
||||
// Automatically refresh every 15 minutes
|
||||
callback();
|
||||
const interval = setInterval(callback, MINUTES_15);
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
clearInterval(interval);
|
||||
};
|
||||
});
|
||||
|
||||
#patchStacks = derived(this.#apiPatchStacks, (apiPatchStacks) => {
|
||||
return apiPatchStacks.map((apiPatchStack) => new PatchStack(apiPatchStack));
|
||||
});
|
||||
export class CloudPatchStacksService {
|
||||
#apiPatchStacks: Writable<ApiPatchStack[]>;
|
||||
#patchStacks;
|
||||
|
||||
constructor(
|
||||
private readonly _repositoryId: string,
|
||||
private readonly repositoryId: Readable<string | undefined>,
|
||||
private readonly patchStacksApiService: PatchStacksApiService
|
||||
) {}
|
||||
) {
|
||||
this.#apiPatchStacks = writableDerived<ApiPatchStack[], string | undefined>(
|
||||
this.repositoryId,
|
||||
[],
|
||||
(repositoryId, set) => {
|
||||
if (!repositoryId) {
|
||||
set([]);
|
||||
return;
|
||||
}
|
||||
|
||||
let canceled = false;
|
||||
|
||||
const callback = (() => {
|
||||
this.patchStacksApiService.getPatchStacks(repositoryId).then((patchStacks) => {
|
||||
if (!canceled) set(patchStacks);
|
||||
});
|
||||
}).bind(this);
|
||||
|
||||
// Automatically refresh every 15 minutes
|
||||
callback();
|
||||
const interval = setInterval(callback, MINUTES_15);
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
clearInterval(interval);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
this.#patchStacks = derived(this.#apiPatchStacks, (apiPatchStacks) => {
|
||||
return apiPatchStacks.map((apiPatchStack) => new CloudPatchStack(apiPatchStack));
|
||||
});
|
||||
}
|
||||
|
||||
/** An unordered list of patch stacks for a given repository */
|
||||
get patchStacks(): Readable<PatchStack[]> {
|
||||
get patchStacks(): Readable<CloudPatchStack[]> {
|
||||
return this.#patchStacks;
|
||||
}
|
||||
|
||||
/** Refresh the list of patch stacks */
|
||||
async refresh(): Promise<void> {
|
||||
const patchStacks = await this.patchStacksApiService.getPatchStacks();
|
||||
const repositoryId = get(this.repositoryId);
|
||||
if (!repositoryId) {
|
||||
this.#apiPatchStacks.set([]);
|
||||
return;
|
||||
}
|
||||
const patchStacks = await this.patchStacksApiService.getPatchStacks(repositoryId);
|
||||
this.#apiPatchStacks.set(patchStacks);
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from '$lib/context';
|
||||
import { PatchStacksService } from '$lib/stacks/service';
|
||||
|
||||
/**
|
||||
* Expects the following contexts:
|
||||
* - PatchStacksService
|
||||
*/
|
||||
|
||||
const _patchStacksService = getContext(PatchStacksService);
|
||||
</script>
|
57
packages/shared/src/lib/storeUtils.ts
Normal file
57
packages/shared/src/lib/storeUtils.ts
Normal file
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* This module contains some utilities primarily inspired by RxJS that can
|
||||
* help us avoid manually managing subscriptions and unsubscriptions in
|
||||
* application code.
|
||||
*/
|
||||
|
||||
import { writable, type Readable } from 'svelte/store';
|
||||
|
||||
export function combineLatest<T>(stores: Readable<T>[], initialValue: T): Readable<T> {
|
||||
const store = writable(initialValue, (set) => {
|
||||
const unsubscribers = stores.map((store) => store.subscribe(set));
|
||||
return () => {
|
||||
unsubscribers.forEach((unsubscribe) => unsubscribe());
|
||||
};
|
||||
});
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like a writable, but the startStopNotifier gets re-run whenever the provided store changes.
|
||||
*
|
||||
* If the source store is changed frequently, consider throttling or debouncing it as it will
|
||||
* cause the StartStopNotifier to be called frequently.
|
||||
*
|
||||
* Example lifecycle:
|
||||
*
|
||||
* o Readable = 1
|
||||
* |
|
||||
* o - o writableDerived subscribed (ssn called, passed value 1, ssn returns 3) wD, = 3
|
||||
* | |
|
||||
* o o Readable updated to 2, (ssn unsubscribers called, ssn called, passed value 2, ssn returns 4), wD = 4
|
||||
* | |
|
||||
* | o writable derived set to 6, wD = 6
|
||||
* | |
|
||||
* | o writableDerived unsubscribed (ssn unsubscribers called)
|
||||
*/
|
||||
export function writableDerived<A, B>(
|
||||
store: Readable<B>,
|
||||
initialValue: A,
|
||||
startStopNotifier: (derivedValue: B, set: (value: A) => void) => (() => void) | undefined
|
||||
) {
|
||||
const derivedStore = writable(initialValue, (set) => {
|
||||
let startStopUnsubscribe: (() => void) | undefined = undefined;
|
||||
const unsubscribeStore = store.subscribe((value) => {
|
||||
startStopUnsubscribe?.();
|
||||
startStopUnsubscribe = startStopNotifier(value, set);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribeStore();
|
||||
startStopUnsubscribe?.();
|
||||
};
|
||||
});
|
||||
|
||||
return derivedStore;
|
||||
}
|
Loading…
Reference in New Issue
Block a user