Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/lib/utils/wave/types/waveProgram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export const waveProgramRepoWithDetailsDtoSchema = z.object({
pointsUsed: z.number().int(),
pointsBudget: z.number().int().nullable(),
pointsRemaining: z.number().int().nullable(),
pointsBudgetOverride: z.number().int().nullable().optional(),
pointsMultiplier: z.number().int().optional(),
repo: z.object({
stargazersCount: z.number().int().nullable().optional(),
Expand Down
30 changes: 30 additions & 0 deletions src/lib/utils/wave/wavePrograms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,36 @@ export async function unfeatureWaveProgramRepo(
});
}

export async function setRepoBudgetOverride(
f = fetch,
waveProgramId: string,
orgRepoId: string,
pointsBudget: number,
) {
await authenticatedCall(
f,
`/api/wave-programs/${waveProgramId}/repos/${orgRepoId}/budget-override`,
{
method: 'PUT',
body: JSON.stringify({ pointsBudget }),
},
);
}

export async function removeRepoBudgetOverride(
f = fetch,
waveProgramId: string,
orgRepoId: string,
) {
await authenticatedCall(
f,
`/api/wave-programs/${waveProgramId}/repos/${orgRepoId}/budget-override`,
{
method: 'DELETE',
},
);
}

export async function addIssueToWaveProgram(
f = fetch,
waveProgramId: string,
Expand Down
4 changes: 3 additions & 1 deletion src/routes/(pages)/wave/(base-layout)/admin/repos/+layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ export const load = async ({ parent, fetch, depends }) => {

const canManageTags = user.permissions?.includes('manageTags');
const canFeatureRepos = user.permissions?.includes('featureWaveRepos');
const canManagePoints = user.permissions?.includes('managePoints');

if (!canManageTags && !canFeatureRepos) {
if (!canManageTags && !canFeatureRepos && !canManagePoints) {
throw redirect(302, '/wave/admin');
}

Expand All @@ -24,5 +25,6 @@ export const load = async ({ parent, fetch, depends }) => {
tags,
canManageTags: !!canManageTags,
canFeatureRepos: !!canFeatureRepos,
canManagePoints: !!canManagePoints,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<script lang="ts">
import Button from '$lib/components/button/button.svelte';
import TextInput from '$lib/components/text-input/text-input.svelte';
import FormField from '$lib/components/form-field/form-field.svelte';
import StandaloneFlowStepLayout from '$lib/components/standalone-flow-step-layout/standalone-flow-step-layout.svelte';
import AnnotationBox from '$lib/components/annotation-box/annotation-box.svelte';
import Coin from '$lib/components/icons/Coin.svelte';
import modal from '$lib/stores/modal';
import { setRepoBudgetOverride, removeRepoBudgetOverride } from '$lib/utils/wave/wavePrograms';
import type { WaveProgramRepoWithDetailsDto } from '$lib/utils/wave/types/waveProgram';

interface Props {
repo: WaveProgramRepoWithDetailsDto;
waveProgramId: string;
onChanged: () => void;
}

let { repo, waveProgramId, onChanged }: Props = $props();

let hasExistingOverride = $derived(
repo.pointsBudgetOverride != null && repo.pointsBudgetOverride > 0,
);
Comment thread
jtourkos marked this conversation as resolved.
Outdated

let budget = $state(hasExistingOverride ? String(repo.pointsBudgetOverride) : '');
let submitting = $state(false);
let error = $state<string | null>(null);

let budgetNum = $derived(parseInt(budget, 10));
let canSubmit = $derived(!isNaN(budgetNum) && budgetNum >= 1 && !submitting);
Comment thread
jtourkos marked this conversation as resolved.
Outdated

async function handleSubmit() {
if (!canSubmit) return;

submitting = true;
error = null;

try {
await setRepoBudgetOverride(fetch, waveProgramId, repo.repo.id, budgetNum);
onChanged();
modal.hide();
} catch (e) {
error = e instanceof Error ? e.message : 'An unexpected error occurred.';
} finally {
submitting = false;
}
}

async function handleRemove() {
submitting = true;
error = null;

try {
await removeRepoBudgetOverride(fetch, waveProgramId, repo.repo.id);
onChanged();
modal.hide();
} catch (e) {
error = e instanceof Error ? e.message : 'An unexpected error occurred.';
} finally {
submitting = false;
}
}
</script>

<div class="modal">
<StandaloneFlowStepLayout headline="Budget override" description={repo.repo.gitHubRepoFullName}>
<div class="fields">
<FormField
title="Points budget"
description="Override the wave program's default per-repo budget for this repo. Leave empty and remove to use the program default."
>
<TextInput
bind:value={budget}
placeholder={repo.pointsBudget != null ? String(repo.pointsBudget) : 'Unlimited'}
variant={{ type: 'number', min: 1 }}
/>
</FormField>
</div>

{#if error}
<AnnotationBox type="error">{error}</AnnotationBox>
{/if}

{#snippet actions()}
{#if hasExistingOverride}
<Button variant="destructive-outline" disabled={submitting} onclick={handleRemove}
>Remove override</Button
>
{/if}
<Button variant="normal" disabled={submitting} onclick={modal.hide}>Cancel</Button>
<Button
variant="primary"
icon={Coin}
loading={submitting}
disabled={!canSubmit}
onclick={handleSubmit}
>
Set budget to {budgetNum || '?'}
</Button>
{/snippet}
</StandaloneFlowStepLayout>
</div>

<style>
.modal {
padding: 1rem;
}

.fields {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@
import type { Pagination } from '$lib/utils/wave/types/pagination';
import ManageRepoTagsModal from '../components/manage-repo-tags-modal.svelte';
import FeatureRepoModal from '../components/feature-repo-modal.svelte';
import BudgetOverrideModal from '../components/budget-override-modal.svelte';
import Coin from '$lib/components/icons/Coin.svelte';

let { data } = $props();

let tags = $derived(data.tags);
let wavePrograms = $derived(data.wavePrograms);
let canManageTags = $derived(data.canManageTags);
let canFeatureRepos = $derived(data.canFeatureRepos);
let canManagePoints = $derived(data.canManagePoints);

let waveProgramId = $derived(data.waveProgramId);
let repos = $derived(data.repos);
Expand Down Expand Up @@ -123,6 +126,14 @@
});
}

function openBudgetOverrideModal(repo: WaveProgramRepoWithDetailsDto) {
modal.show(BudgetOverrideModal, undefined, {
repo,
waveProgramId: waveProgramId!,
onChanged: reloadPage,
});
}

async function handleUnfeature(repo: WaveProgramRepoWithDetailsDto) {
await doWithConfirmationModal(
`Remove featured status from ${repo.repo.gitHubRepoFullName}?`,
Expand Down Expand Up @@ -196,6 +207,18 @@
>
{/if}
{/if}

{#if canManagePoints}
<Button
size="small"
variant={repoWithDetails.pointsBudgetOverride != null ? 'caution' : 'normal'}
icon={Coin}
onclick={() => openBudgetOverrideModal(repoWithDetails)}
>{repoWithDetails.pointsBudgetOverride != null
Comment thread
jtourkos marked this conversation as resolved.
? `Budget: ${repoWithDetails.pointsBudgetOverride}`
: 'Budget'}</Button
>
{/if}
</div>
{/snippet}
</RepoPreviewCard>
Expand Down
Loading