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:
@@ -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
@@ -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 }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user