forked from retoor/devplacepy
feat: add optional ticket selection to planning report generation
Add a `numbers` form field to the planning report endpoint, allowing admins to generate a report for a specific subset of open tickets instead of always planning all open tickets. The selected numbers are parsed from a comma-separated string, deduplicated, and passed to the background job. The job's scope is recorded in the audit log and the Devii tool description is updated to document the new parameter.
This commit is contained in:
@@ -41,6 +41,82 @@
|
||||
}
|
||||
}
|
||||
|
||||
.planning-select {
|
||||
margin: 1.5rem 0;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.planning-select-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.85rem 1.25rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.planning-select-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.planning-select-count {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.planning-ticket-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-height: 22rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.planning-ticket {
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.planning-ticket:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.planning-ticket label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.6rem 1.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.planning-ticket-number {
|
||||
color: var(--text-secondary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.planning-ticket-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.planning-ticket-label {
|
||||
padding: 0.1rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.planning-result {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,48 @@ export class PlanningGenerator {
|
||||
event.preventDefault();
|
||||
this.generate(button);
|
||||
});
|
||||
document.addEventListener("change", (event) => {
|
||||
if (event.target.closest("[data-planning-select-all]")) {
|
||||
this.toggleAll(event.target.checked);
|
||||
this.syncSelection();
|
||||
} else if (event.target.closest("[data-planning-ticket]")) {
|
||||
this.syncSelection();
|
||||
}
|
||||
});
|
||||
this.syncSelection();
|
||||
}
|
||||
|
||||
tickets() {
|
||||
return Array.from(document.querySelectorAll("[data-planning-ticket]"));
|
||||
}
|
||||
|
||||
selectedNumbers() {
|
||||
return this.tickets()
|
||||
.filter((box) => box.checked)
|
||||
.map((box) => box.value);
|
||||
}
|
||||
|
||||
toggleAll(checked) {
|
||||
this.tickets().forEach((box) => {
|
||||
box.checked = checked;
|
||||
});
|
||||
}
|
||||
|
||||
syncSelection() {
|
||||
const tickets = this.tickets();
|
||||
if (!tickets.length) return;
|
||||
const selected = tickets.filter((box) => box.checked).length;
|
||||
const count = document.querySelector("[data-planning-count]");
|
||||
if (count) {
|
||||
count.textContent = `${selected} of ${tickets.length} selected`;
|
||||
}
|
||||
const master = document.querySelector("[data-planning-select-all]");
|
||||
if (master) {
|
||||
master.checked = selected === tickets.length;
|
||||
master.indeterminate = selected > 0 && selected < tickets.length;
|
||||
}
|
||||
const button = document.querySelector("[data-planning-generate]");
|
||||
if (button) button.disabled = selected === 0;
|
||||
}
|
||||
|
||||
elements() {
|
||||
@@ -26,6 +68,11 @@ export class PlanningGenerator {
|
||||
|
||||
async generate(button) {
|
||||
if (this.active) return;
|
||||
const numbers = this.selectedNumbers();
|
||||
if (this.tickets().length && !numbers.length) {
|
||||
window.app.toast.show("Select at least one ticket", { type: "error" });
|
||||
return;
|
||||
}
|
||||
this.active = true;
|
||||
button.disabled = true;
|
||||
button.classList.add("is-loading");
|
||||
@@ -35,7 +82,7 @@ export class PlanningGenerator {
|
||||
if (ui.status) ui.status.hidden = false;
|
||||
try {
|
||||
const action = button.dataset.action || "/issues/planning";
|
||||
const job = await Http.sendForm(action, {});
|
||||
const job = await Http.sendForm(action, { numbers: numbers.join(",") });
|
||||
await this.poll(job.status_url, ui);
|
||||
} catch (error) {
|
||||
window.app.toast.show("Could not start the planning report", { type: "error" });
|
||||
@@ -45,6 +92,7 @@ export class PlanningGenerator {
|
||||
this.active = false;
|
||||
button.disabled = false;
|
||||
button.classList.remove("is-loading");
|
||||
this.syncSelection();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user