feat(admin): style search + clarify bulk actions

This commit is contained in:
Young Lee
2026-02-06 00:49:36 -08:00
parent 65cf54a764
commit 2d350a7601
5 changed files with 205 additions and 122 deletions
+165 -116
View File
@@ -352,35 +352,6 @@ app.get("/", async (c) => {
: view === "table"
? html`
<div class="card">
<div class="toolbar">
<div class="toolbar-group toolbar-group-fill">
<input
type="search"
id="feed-search"
class="search"
placeholder="Search title, feed id, or description"
oninput="scheduleFeedFilter()"
/>
<button
type="button"
class="button button-small"
onclick="setVisibleFeedSelection(true)"
>
Select Visible
</button>
<button
type="button"
class="button button-small"
onclick="setVisibleFeedSelection(false)"
>
Clear Visible
</button>
<span class="pill" id="selected-feed-count"
>0 selected</span
>
</div>
</div>
<form
id="bulk-feed-delete-form"
action="/admin/feeds/bulk-delete"
@@ -389,6 +360,46 @@ app.get("/", async (c) => {
>
<input type="hidden" name="view" value="table" />
<div class="toolbar">
<div class="toolbar-group toolbar-group-fill">
<input
type="search"
id="feed-search"
class="search"
placeholder="Search title, feed id, or description"
oninput="scheduleFeedFilter()"
/>
<span class="pill" id="feed-match-count"
>Showing ${feedsWithConfig.length}</span
>
<span class="pill" id="selected-feed-count"
>0 selected</span
>
<button
type="button"
class="button button-small button-secondary"
onclick="selectMatchingFeeds()"
>
Select Results
</button>
<button
type="button"
class="button button-small button-secondary"
onclick="clearFeedSelection()"
>
Clear Selection
</button>
<button
id="bulk-delete-feeds-button"
type="submit"
class="button button-small button-danger"
disabled
>
Delete Selected
</button>
</div>
</div>
<div class="table-wrap">
<table class="table table-feeds">
<colgroup>
@@ -617,17 +628,6 @@ app.get("/", async (c) => {
</tbody>
</table>
</div>
<div class="actions-row">
<button
id="bulk-delete-feeds-button"
type="submit"
class="button button-danger"
disabled
>
Delete Selected Feeds
</button>
</div>
</form>
</div>
`
@@ -810,6 +810,7 @@ app.get("/", async (c) => {
let FEED_ROWS = [];
let FEED_CHECKBOXES = [];
let FEED_SELECTED_COUNT_EL = null;
let FEED_MATCH_COUNT_EL = null;
let FEED_BULK_DELETE_BUTTON_EL = null;
let FEED_SELECT_ALL_EL = null;
let FEED_FILTER_TIMER = null;
@@ -821,13 +822,23 @@ app.get("/", async (c) => {
FEED_ROWS = Array.from(document.querySelectorAll('.feed-row'));
FEED_CHECKBOXES = Array.from(document.querySelectorAll('.feed-select'));
FEED_SELECTED_COUNT_EL = document.getElementById('selected-feed-count');
FEED_MATCH_COUNT_EL = document.getElementById('feed-match-count');
FEED_BULK_DELETE_BUTTON_EL = document.getElementById('bulk-delete-feeds-button');
FEED_SELECT_ALL_EL = document.getElementById('select-all-feeds');
setupFeedTableResizing();
setupFeedTableSorting();
updateFeedMatchCount();
updateFeedSelectionState();
}
function updateFeedMatchCount() {
if (!FEED_MATCH_COUNT_EL) return;
const total = FEED_ROWS.length;
const visible = FEED_ROWS.filter((row) => !row.hidden).length;
const query = (document.getElementById('feed-search')?.value || '').trim();
FEED_MATCH_COUNT_EL.textContent = query ? ('Showing ' + visible + ' of ' + total) : ('Showing ' + total);
}
function scheduleFeedFilter() {
if (FEED_FILTER_TIMER) {
clearTimeout(FEED_FILTER_TIMER);
@@ -1052,12 +1063,24 @@ app.get("/", async (c) => {
updateFeedSelectionState();
}
function selectMatchingFeeds() {
setVisibleFeedSelection(true);
}
function clearFeedSelection() {
FEED_CHECKBOXES.forEach((checkbox) => {
checkbox.checked = false;
})
updateFeedSelectionState();
}
function filterFeedRows() {
const query = (document.getElementById('feed-search')?.value || '').toLowerCase().trim();
FEED_ROWS.forEach((row) => {
const haystack = row.getAttribute('data-search') || '';
row.hidden = !!query && !haystack.includes(query);
});
updateFeedMatchCount();
updateFeedSelectionState();
}
@@ -1520,38 +1543,51 @@ app.get("/feeds/:feedId/emails", async (c) => {
: ""}
${feedMetadata.emails.length > 0
? html`
<div class="toolbar">
<div class="toolbar-group toolbar-group-fill">
<input
type="search"
id="email-search"
class="search"
placeholder="Search email subjects"
oninput="scheduleEmailFilter()"
/>
<button
type="button"
class="button button-small"
onclick="setVisibleEmailSelection(true)"
>
Select Visible
</button>
<button
type="button"
class="button button-small"
onclick="setVisibleEmailSelection(false)"
>
Clear Visible
</button>
<span class="pill" id="selected-email-count">0 selected</span>
</div>
</div>
<form
action="/admin/feeds/${feedId}/emails/bulk-delete"
method="post"
onsubmit="return confirmBulkEmailDelete()"
>
<div class="toolbar">
<div class="toolbar-group toolbar-group-fill">
<input
type="search"
id="email-search"
class="search"
placeholder="Search email subjects"
oninput="scheduleEmailFilter()"
/>
<span class="pill" id="email-match-count"
>Showing ${feedMetadata.emails.length}</span
>
<span class="pill" id="selected-email-count"
>0 selected</span
>
<button
type="button"
class="button button-small button-secondary"
onclick="selectMatchingEmails()"
>
Select Results
</button>
<button
type="button"
class="button button-small button-secondary"
onclick="clearEmailSelection()"
>
Clear Selection
</button>
<button
id="bulk-delete-emails-button"
type="submit"
class="button button-small button-danger"
disabled
>
Delete Selected
</button>
</div>
</div>
<div class="table-wrap">
<table class="table table-emails">
<colgroup>
@@ -1641,16 +1677,6 @@ app.get("/feeds/:feedId/emails", async (c) => {
</tbody>
</table>
</div>
<div class="actions-row">
<button
id="bulk-delete-emails-button"
type="submit"
class="button button-danger"
disabled
>
Delete Selected Emails
</button>
</div>
</form>
`
: html`<div class="card">
@@ -1663,32 +1689,43 @@ app.get("/feeds/:feedId/emails", async (c) => {
<script>
${raw(`
let EMAIL_ROWS = [];
let EMAIL_CHECKBOXES = [];
let EMAIL_SELECTED_COUNT_EL = null;
let EMAIL_BULK_DELETE_BUTTON_EL = null;
let EMAIL_SELECT_ALL_EL = null;
let EMAIL_FILTER_TIMER = null;
let EMAIL_SORT_KEY = 'receivedAt';
let EMAIL_SORT_DIR = 'desc';
let EMAIL_ROWS = [];
let EMAIL_CHECKBOXES = [];
let EMAIL_SELECTED_COUNT_EL = null;
let EMAIL_MATCH_COUNT_EL = null;
let EMAIL_BULK_DELETE_BUTTON_EL = null;
let EMAIL_SELECT_ALL_EL = null;
let EMAIL_FILTER_TIMER = null;
let EMAIL_SORT_KEY = 'receivedAt';
let EMAIL_SORT_DIR = 'desc';
const EMAIL_COLLATOR = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
function initEmailUI() {
EMAIL_ROWS = Array.from(document.querySelectorAll('.email-row'));
EMAIL_CHECKBOXES = Array.from(document.querySelectorAll('.email-select'));
EMAIL_SELECTED_COUNT_EL = document.getElementById('selected-email-count');
EMAIL_BULK_DELETE_BUTTON_EL = document.getElementById('bulk-delete-emails-button');
EMAIL_SELECT_ALL_EL = document.getElementById('select-all-emails');
setupEmailTableResizing();
setupEmailTableSorting();
updateEmailSelectionState();
}
function initEmailUI() {
EMAIL_ROWS = Array.from(document.querySelectorAll('.email-row'));
EMAIL_CHECKBOXES = Array.from(document.querySelectorAll('.email-select'));
EMAIL_SELECTED_COUNT_EL = document.getElementById('selected-email-count');
EMAIL_MATCH_COUNT_EL = document.getElementById('email-match-count');
EMAIL_BULK_DELETE_BUTTON_EL = document.getElementById('bulk-delete-emails-button');
EMAIL_SELECT_ALL_EL = document.getElementById('select-all-emails');
setupEmailTableResizing();
setupEmailTableSorting();
updateEmailMatchCount();
updateEmailSelectionState();
}
function scheduleEmailFilter() {
if (EMAIL_FILTER_TIMER) {
clearTimeout(EMAIL_FILTER_TIMER);
}
EMAIL_FILTER_TIMER = setTimeout(filterEmailRows, 120);
function updateEmailMatchCount() {
if (!EMAIL_MATCH_COUNT_EL) return;
const total = EMAIL_ROWS.length;
const visible = EMAIL_ROWS.filter((row) => !row.hidden).length;
const query = (document.getElementById('email-search')?.value || '').trim();
EMAIL_MATCH_COUNT_EL.textContent = query ? ('Showing ' + visible + ' of ' + total) : ('Showing ' + total);
}
function scheduleEmailFilter() {
if (EMAIL_FILTER_TIMER) {
clearTimeout(EMAIL_FILTER_TIMER);
}
EMAIL_FILTER_TIMER = setTimeout(filterEmailRows, 120);
}
function getEmailSortValue(row, key) {
@@ -1894,23 +1931,35 @@ app.get("/feeds/:feedId/emails", async (c) => {
updateEmailSelectionState();
}
function setVisibleEmailSelection(checked) {
EMAIL_CHECKBOXES.forEach((checkbox) => {
if (!checkbox.closest('tr')?.hidden) {
checkbox.checked = checked;
}
})
updateEmailSelectionState();
}
function setVisibleEmailSelection(checked) {
EMAIL_CHECKBOXES.forEach((checkbox) => {
if (!checkbox.closest('tr')?.hidden) {
checkbox.checked = checked;
}
})
updateEmailSelectionState();
}
function filterEmailRows() {
const query = (document.getElementById('email-search')?.value || '').toLowerCase().trim();
EMAIL_ROWS.forEach((row) => {
const haystack = row.getAttribute('data-search') || '';
row.hidden = !!query && !haystack.includes(query);
});
updateEmailSelectionState();
}
function selectMatchingEmails() {
setVisibleEmailSelection(true);
}
function clearEmailSelection() {
EMAIL_CHECKBOXES.forEach((checkbox) => {
checkbox.checked = false;
})
updateEmailSelectionState();
}
function filterEmailRows() {
const query = (document.getElementById('email-search')?.value || '').toLowerCase().trim();
EMAIL_ROWS.forEach((row) => {
const haystack = row.getAttribute('data-search') || '';
row.hidden = !!query && !haystack.includes(query);
});
updateEmailMatchCount();
updateEmailSelectionState();
}
function confirmBulkEmailDelete() {
const selected = EMAIL_CHECKBOXES.filter((checkbox) => checkbox.checked).length;
+33
View File
@@ -240,6 +240,7 @@ export const componentStyles = `
input[type="text"],
input[type="email"],
input[type="search"],
input[type="password"],
textarea {
display: block;
@@ -256,6 +257,18 @@ export const componentStyles = `
backdrop-filter: blur(var(--blur-sm));
-webkit-backdrop-filter: blur(var(--blur-sm));
}
input[type="search"] {
-webkit-appearance: none;
appearance: none;
}
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-results-button,
input[type="search"]::-webkit-search-results-decoration {
-webkit-appearance: none;
}
input:focus,
textarea:focus {
@@ -507,6 +520,26 @@ export const componentStyles = `
input.search {
min-width: 280px;
flex: 1;
height: 40px;
border-radius: var(--radius-pill);
padding: 0 14px;
padding-left: 38px;
background-color: rgba(60, 60, 67, 0.14);
background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%238e8e93'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Ccircle%20cx='11'%20cy='11'%20r='7'/%3E%3Cpath%20d='M21%2021l-4.3-4.3'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: 14px center;
background-size: 16px 16px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
@media (prefers-color-scheme: light) {
input.search {
background-color: rgba(60, 60, 67, 0.08);
}
}
input.search::placeholder {
color: var(--color-text-tertiary);
}
.actions-row {
+4 -4
View File
@@ -36,10 +36,10 @@ export const layoutStyles = `
box-sizing: border-box;
}
/* Wider layout for data-dense pages (tables) */
.container-wide {
max-width: 1280px;
}
/* Wider layout for data-dense pages (tables) */
.container-wide {
max-width: 1440px;
}
/* Header Styles */
.header {