Replace thread status dropdown/text with inline status buttons

Card and modal both use a clickable button row for status instead of
static text or a select, so changing status no longer requires
opening the full edit form.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
claudecode
2026-07-01 18:24:53 -04:00
parent 85adbbf084
commit e61328aa4e
2 changed files with 171 additions and 15 deletions
+65
View File
@@ -1042,6 +1042,39 @@ h1, h2, h3, h4, h5, h6 {
white-space: pre-wrap; white-space: pre-wrap;
} }
.thread-status-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 2px;
}
.thread-status-btn {
padding: 6px 14px;
border: 1px solid var(--border);
border-radius: 3px;
background: var(--bg);
color: var(--text-secondary);
font-family: var(--font-heading);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
cursor: pointer;
transition: border-color var(--transition-speed) ease, color var(--transition-speed) ease, background var(--transition-speed) ease;
}
.thread-status-btn:hover {
color: var(--text-primary);
border-color: var(--accent-hover);
}
.thread-status-btn.active {
border-color: var(--accent);
color: var(--accent);
background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.thread-card-actions { .thread-card-actions {
display: flex; display: flex;
gap: 10px; gap: 10px;
@@ -1162,6 +1195,38 @@ h1, h2, h3, h4, h5, h6 {
border-color: var(--accent-hover); border-color: var(--accent-hover);
} }
.modal-status-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.modal-status-btn {
padding: 8px 14px;
border: 1px solid var(--border);
border-radius: 3px;
background: var(--bg);
color: var(--text-secondary);
font-family: var(--font-heading);
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
cursor: pointer;
transition: border-color var(--transition-speed) ease, color var(--transition-speed) ease, background var(--transition-speed) ease;
}
.modal-status-btn:hover {
color: var(--text-primary);
border-color: var(--accent-hover);
}
.modal-status-btn.active {
border-color: var(--accent);
color: var(--accent);
background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.modal-actions { .modal-actions {
display: flex; display: flex;
gap: 10px; gap: 10px;
+106 -15
View File
@@ -133,17 +133,22 @@ function buildThreadCard(thread) {
if (expandedId === thread.id) { if (expandedId === thread.id) {
card.classList.add('expanded'); card.classList.add('expanded');
card.appendChild(buildThreadDetail(thread)); card.appendChild(buildThreadDetail(thread, card));
} }
return card; return card;
} }
function buildThreadDetail(thread) { function buildThreadDetail(thread, card) {
const detail = document.createElement('div'); const detail = document.createElement('div');
detail.className = 'thread-card-detail'; detail.className = 'thread-card-detail';
FIELD_DEFS.forEach(({ key, label }) => { FIELD_DEFS.forEach(({ key, label }) => {
if (key === 'status') {
detail.appendChild(buildStatusButtonRow(thread, card));
return;
}
const row = document.createElement('div'); const row = document.createElement('div');
row.className = 'thread-field'; row.className = 'thread-field';
@@ -153,7 +158,7 @@ function buildThreadDetail(thread) {
const valueEl = document.createElement('span'); const valueEl = document.createElement('span');
valueEl.className = 'thread-field-value'; valueEl.className = 'thread-field-value';
valueEl.textContent = key === 'status' ? statusLabel(thread.status) : (thread[key] || '—'); valueEl.textContent = thread[key] || '—';
row.append(labelEl, valueEl); row.append(labelEl, valueEl);
detail.appendChild(row); detail.appendChild(row);
@@ -180,6 +185,66 @@ function buildThreadDetail(thread) {
return detail; return detail;
} }
function buildStatusButtonRow(thread, card) {
const row = document.createElement('div');
row.className = 'thread-field';
const labelEl = document.createElement('span');
labelEl.className = 'thread-field-label';
labelEl.textContent = 'Status';
row.appendChild(labelEl);
const buttonRow = document.createElement('div');
buttonRow.className = 'thread-status-buttons';
STATUSES.forEach((status) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'thread-status-btn';
btn.dataset.status = status;
btn.classList.toggle('active', status === thread.status);
btn.textContent = statusLabel(status);
btn.addEventListener('click', () => handleStatusChange(thread, status, card));
buttonRow.appendChild(btn);
});
row.appendChild(buttonRow);
return row;
}
async function handleStatusChange(thread, newStatus, card) {
if (newStatus === thread.status) return;
const campaign = getActiveCampaign();
if (!campaign) return;
const payload = {};
FIELD_DEFS.forEach(({ key }) => {
payload[key] = thread[key];
});
payload.status = newStatus;
try {
const updated = await updateThread(campaign.id, thread.id, payload);
Object.assign(thread, updated);
updateCardStatusUI(card, thread);
} catch (err) {
alert(err.message);
}
}
function updateCardStatusUI(card, thread) {
const badge = card.querySelector('.thread-status-badge');
if (badge) {
badge.className = `thread-status-badge thread-status-${thread.status}`;
badge.textContent = statusLabel(thread.status);
}
card.querySelectorAll('.thread-status-btn').forEach((btn) => {
btn.classList.toggle('active', btn.dataset.status === thread.status);
});
}
function buildModalForm(values) { function buildModalForm(values) {
const body = container.querySelector('#threadModalBody'); const body = container.querySelector('#threadModalBody');
body.innerHTML = ''; body.innerHTML = '';
@@ -191,21 +256,18 @@ function buildModalForm(values) {
const labelEl = document.createElement('label'); const labelEl = document.createElement('label');
labelEl.className = 'modal-field-label'; labelEl.className = 'modal-field-label';
labelEl.textContent = label; labelEl.textContent = label;
labelEl.setAttribute('for', `thread-field-${key}`);
group.appendChild(labelEl); group.appendChild(labelEl);
let input;
if (type === 'select') { if (type === 'select') {
input = document.createElement('select'); group.appendChild(buildModalStatusButtons(values.status || 'active'));
input.className = 'modal-select'; body.appendChild(group);
STATUSES.forEach((status) => { return;
const option = document.createElement('option'); }
option.value = status;
option.textContent = statusLabel(status); labelEl.setAttribute('for', `thread-field-${key}`);
input.appendChild(option);
}); let input;
input.value = values.status || 'active'; if (type === 'textarea') {
} else if (type === 'textarea') {
input = document.createElement('textarea'); input = document.createElement('textarea');
input.className = 'modal-textarea'; input.className = 'modal-textarea';
input.rows = 3; input.rows = 3;
@@ -223,6 +285,35 @@ function buildModalForm(values) {
}); });
} }
function buildModalStatusButtons(currentStatus) {
const wrapper = document.createElement('div');
wrapper.className = 'modal-status-buttons';
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.id = 'thread-field-status';
hiddenInput.value = currentStatus;
wrapper.appendChild(hiddenInput);
STATUSES.forEach((status) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'modal-status-btn';
btn.dataset.status = status;
btn.classList.toggle('active', status === currentStatus);
btn.textContent = statusLabel(status);
btn.addEventListener('click', () => {
hiddenInput.value = status;
wrapper.querySelectorAll('.modal-status-btn').forEach((b) => {
b.classList.toggle('active', b.dataset.status === status);
});
});
wrapper.appendChild(btn);
});
return wrapper;
}
function collectFormData() { function collectFormData() {
const data = {}; const data = {};
FIELD_DEFS.forEach(({ key }) => { FIELD_DEFS.forEach(({ key }) => {