Skip to content

Commit b42baa6

Browse files
committed
Them chon MRG khi merge PR
1 parent f8ab78e commit b42baa6

9 files changed

Lines changed: 308 additions & 26 deletions

File tree

admin/src/App.vue

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@
266266
<small>{{ task.issue_url ? `Issue #${task.issue_number}` : task.project_id }}</small>
267267
</td>
268268
<td>{{ task.required_worker_kind }}</td>
269-
<td>{{ money(task.reward_cents) }}</td>
269+
<td>{{ mrg(task.reward_cents) }}</td>
270270
<td>{{ task.worker_id || task.suggested_agent_type || 'Unassigned' }}</td>
271271
<td><span :class="['status-pill', task.status === 'accepted' ? 'blue' : 'amber']">{{ task.status }}</span></td>
272272
<td class="task-pr-cell">
@@ -288,9 +288,27 @@
288288
<div>
289289
<strong>#{{ pull.number }} {{ pull.title }}</strong>
290290
<small>@{{ pull.author }} / {{ pullStatus(pull) }} / {{ pull.head_ref || 'head' }} -> {{ pull.base_ref || 'base' }}</small>
291-
<em>Credit: github:{{ pull.author }}</em>
291+
<em>Credit: github:{{ pull.author }} / {{ mrg(mergeSelection(task, pull).reward_mrg) }}</em>
292292
</div>
293293
</div>
294+
<div class="bounty-review-controls">
295+
<label>
296+
<span>Type</span>
297+
<select :value="mergeSelection(task, pull).bounty_type" @change="setMergeBounty(task, pull, $event.target.value)">
298+
<option v-for="option in bountyOptions" :key="option.id" :value="option.id">{{ option.label }}</option>
299+
</select>
300+
</label>
301+
<label>
302+
<span>MRG</span>
303+
<input
304+
:value="mergeSelection(task, pull).reward_mrg"
305+
min="1"
306+
step="1"
307+
type="number"
308+
@input="setMergeReward(task, pull, $event.target.value)"
309+
/>
310+
</label>
311+
</div>
294312
<div class="task-pr-actions">
295313
<a class="compact-action link-action" :href="pull.html_url" target="_blank" rel="noreferrer">View</a>
296314
<button class="compact-action merge-action" :disabled="!canMergeTaskPull(task, pull)" type="button" @click="mergeTaskPull(task, pull)">
@@ -523,6 +541,7 @@ const taskPullsLoading = ref({});
523541
const taskPullsError = ref({});
524542
const mergeBusy = ref({});
525543
const mergeMessages = ref({});
544+
const mergeSelections = ref({});
526545
527546
const loginForm = reactive({
528547
email: 'admin@gmail.com',
@@ -563,6 +582,13 @@ const viewByRoute = Object.entries(routeByView).reduce((routes, [view, route]) =
563582
return routes;
564583
}, {});
565584
585+
const bountyOptions = [
586+
{ id: 'future-small', label: 'Future small', reward_mrg: 25 },
587+
{ id: 'future-medium', label: 'Future medium', reward_mrg: 50 },
588+
{ id: 'bug-large', label: 'Bug bounty large', reward_mrg: 100 },
589+
{ id: 'major-feature', label: 'Major feature', reward_mrg: 200 },
590+
];
591+
566592
const builderWidgets = [
567593
{ id: 'metrics', label: 'Metric Counter', icon: BarChart3 },
568594
{ id: 'project-list', label: 'Project Queue', icon: FolderKanban },
@@ -584,6 +610,7 @@ const query = computed(() => search.value.toLowerCase());
584610
const selectedUser = computed(() => users.value.find((row) => row.id === selectedUserId.value) || null);
585611
const sslOkCount = computed(() => sslRows.value.filter((row) => row.status === 'ok').length);
586612
const sslAttentionCount = computed(() => sslRows.value.length - sslOkCount.value);
613+
const tokenSymbol = computed(() => summary.value.token_symbol || 'MRG');
587614
588615
const summaryMetrics = computed(() => [
589616
{ label: 'Users', value: number(summary.value.user_count), icon: UsersRound, tone: 'blue' },
@@ -874,15 +901,69 @@ function mergeKey(task, pull) {
874901
return `${task.id}:${pull.number}`;
875902
}
876903
904+
function defaultBountyForTask(task = {}) {
905+
const reward = Number(task.reward_cents) || 25;
906+
return bountyOptions.find((option) => option.reward_mrg === reward) || bountyOptions[0];
907+
}
908+
909+
function ensureMergeSelection(task, pull) {
910+
if (!task?.id || !pull?.number) return;
911+
const key = mergeKey(task, pull);
912+
if (mergeSelections.value[key]) return;
913+
const option = defaultBountyForTask(task);
914+
mergeSelections.value = {
915+
...mergeSelections.value,
916+
[key]: {
917+
bounty_type: option.id,
918+
reward_mrg: option.reward_mrg,
919+
},
920+
};
921+
}
922+
923+
function mergeSelection(task, pull) {
924+
ensureMergeSelection(task, pull);
925+
return mergeSelections.value[mergeKey(task, pull)] || {
926+
bounty_type: bountyOptions[0].id,
927+
reward_mrg: bountyOptions[0].reward_mrg,
928+
};
929+
}
930+
931+
function setMergeBounty(task, pull, value) {
932+
const option = bountyOptions.find((row) => row.id === value) || bountyOptions[0];
933+
const key = mergeKey(task, pull);
934+
mergeSelections.value = {
935+
...mergeSelections.value,
936+
[key]: {
937+
bounty_type: option.id,
938+
reward_mrg: option.reward_mrg,
939+
},
940+
};
941+
}
942+
943+
function setMergeReward(task, pull, value) {
944+
const key = mergeKey(task, pull);
945+
const current = mergeSelection(task, pull);
946+
const reward = Math.max(1, Math.round(Number(value) || 0));
947+
mergeSelections.value = {
948+
...mergeSelections.value,
949+
[key]: {
950+
...current,
951+
reward_mrg: reward,
952+
},
953+
};
954+
}
955+
877956
function pullStatus(pull) {
878957
if (pull.merged) return 'merged';
879958
if (pull.draft) return 'draft';
880959
return pull.state || 'open';
881960
}
882961
883962
function canMergeTaskPull(task, pull) {
963+
const selection = mergeSelection(task, pull);
884964
if (!pull?.author || task.status === 'accepted') return false;
885965
if (mergeBusy.value[mergeKey(task, pull)] || pull.draft) return false;
966+
if (!selection.bounty_type || Number(selection.reward_mrg) <= 0) return false;
886967
return pull.merged || pull.state === 'open';
887968
}
888969
@@ -897,6 +978,9 @@ async function loadTaskPulls(task, force = false) {
897978
...taskPulls.value,
898979
[task.id]: Array.isArray(payload.pull_requests) ? payload.pull_requests : [],
899980
};
981+
for (const pull of taskPulls.value[task.id]) {
982+
ensureMergeSelection(task, pull);
983+
}
900984
taskPullsLoaded.value = { ...taskPullsLoaded.value, [task.id]: true };
901985
} catch (error) {
902986
taskPullsError.value = { ...taskPullsError.value, [task.id]: error.message };
@@ -924,13 +1008,17 @@ async function loadPullsForVisibleTasks() {
9241008
async function mergeTaskPull(task, pull) {
9251009
if (!canMergeTaskPull(task, pull)) return;
9261010
const key = mergeKey(task, pull);
1011+
const selection = mergeSelection(task, pull);
9271012
mergeBusy.value = { ...mergeBusy.value, [key]: true };
9281013
mergeMessages.value = { ...mergeMessages.value, [task.id]: '' };
9291014
taskPullsError.value = { ...taskPullsError.value, [task.id]: '' };
9301015
try {
9311016
const result = await api(`/api/admin/tasks/${encodeURIComponent(task.id)}/pulls/${pull.number}/merge`, {
9321017
method: 'POST',
933-
body: JSON.stringify({}),
1018+
body: JSON.stringify({
1019+
bounty_type: selection.bounty_type,
1020+
reward_mrg: Number(selection.reward_mrg) || 0,
1021+
}),
9341022
});
9351023
if (result.task) {
9361024
tasks.value = tasks.value.map((row) => (row.id === result.task.id ? result.task : row));
@@ -941,7 +1029,8 @@ async function mergeTaskPull(task, pull) {
9411029
[task.id]: pullsForTask(task).map((row) => (row.number === result.pull_request.number ? result.pull_request : row)),
9421030
};
9431031
}
944-
mergeMessages.value = { ...mergeMessages.value, [task.id]: `Paid ${result.worker_id || `github:${pull.author}`}.` };
1032+
const commentStatus = result.comment_error ? ` Comment failed: ${result.comment_error}` : ' Commented on PR.';
1033+
mergeMessages.value = { ...mergeMessages.value, [task.id]: `Paid ${mrg(result.reward_mrg || selection.reward_mrg)} to ${result.worker_id || `github:${pull.author}`}.${commentStatus}` };
9451034
const [summaryData, ledgerData] = await Promise.all([
9461035
api('/api/admin/summary'),
9471036
api('/api/admin/ledger'),
@@ -985,6 +1074,10 @@ function money(cents = 0) {
9851074
}).format((Number(cents) || 0) / 100);
9861075
}
9871076
1077+
function mrg(value = 0) {
1078+
return `${number(value)} ${tokenSymbol.value}`;
1079+
}
1080+
9881081
function number(value = 0) {
9891082
return new Intl.NumberFormat('en-US').format(Number(value) || 0);
9901083
}

admin/src/styles.css

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,7 @@ button:disabled {
721721

722722
.task-pr-card {
723723
display: grid;
724-
grid-template-columns: minmax(0, 1fr) auto;
724+
grid-template-columns: minmax(0, 1fr) minmax(210px, 280px) auto;
725725
gap: 10px;
726726
align-items: center;
727727
border: 1px solid var(--line);
@@ -760,6 +760,35 @@ button:disabled {
760760
justify-content: flex-end;
761761
}
762762

763+
.bounty-review-controls {
764+
display: grid;
765+
grid-template-columns: minmax(120px, 1fr) 82px;
766+
gap: 8px;
767+
}
768+
769+
.bounty-review-controls label {
770+
display: grid;
771+
gap: 5px;
772+
color: var(--muted);
773+
font-size: 11px;
774+
font-weight: 900;
775+
text-transform: uppercase;
776+
}
777+
778+
.bounty-review-controls select,
779+
.bounty-review-controls input {
780+
width: 100%;
781+
height: 32px;
782+
border: 1px solid var(--line);
783+
border-radius: 8px;
784+
background: #ffffff;
785+
color: var(--text);
786+
padding: 0 9px;
787+
font-size: 12px;
788+
font-weight: 850;
789+
outline: none;
790+
}
791+
763792
.inline-error,
764793
.inline-success,
765794
.muted-inline {
@@ -1033,6 +1062,10 @@ button:disabled {
10331062
grid-template-columns: 1fr;
10341063
}
10351064

1065+
.bounty-review-controls {
1066+
grid-template-columns: 1fr;
1067+
}
1068+
10361069
.task-pr-actions {
10371070
justify-content: flex-start;
10381071
}

0 commit comments

Comments
 (0)