mirror of
https://github.com/simonw/datasette.git
synced 2026-06-01 06:37:02 +02:00
Column actions panel on mobile (#2669)
On mobile widths the column actions were no longer available. This adds a new modal to help with that. https://gisthost.github.io/?ec60eb27e22cf5d96642eec1715586b6
This commit is contained in:
parent
63d73a806f
commit
fd016f7986
5 changed files with 886 additions and 169 deletions
|
|
@ -742,6 +742,284 @@ p.zero-results {
|
|||
.select-wrapper.small-screen-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes datasette-modal-slide-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes datasette-modal-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
dialog.mobile-column-actions-dialog {
|
||||
--ink: #0f0f0f;
|
||||
--paper: #f5f3ef;
|
||||
--muted: #6b6b6b;
|
||||
--rule: #e2dfd8;
|
||||
--accent: #1a56db;
|
||||
--card: #ffffff;
|
||||
border: none;
|
||||
border-radius: var(--modal-border-radius, 0.75rem);
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
width: min(420px, calc(100vw - 32px));
|
||||
max-width: 95vw;
|
||||
max-height: min(640px, calc(100vh - 32px));
|
||||
box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04));
|
||||
animation: datasette-modal-slide-in var(--modal-animation-duration, 0.2s) ease-out;
|
||||
overflow: hidden;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: var(--card);
|
||||
}
|
||||
|
||||
dialog.mobile-column-actions-dialog[open] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
dialog.mobile-column-actions-dialog::backdrop {
|
||||
background: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.5));
|
||||
backdrop-filter: var(--modal-backdrop-blur, blur(4px));
|
||||
-webkit-backdrop-filter: var(--modal-backdrop-blur, blur(4px));
|
||||
animation: datasette-modal-fade-in var(--modal-animation-duration, 0.2s) ease-out;
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .modal-header {
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .modal-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .modal-meta {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.7rem;
|
||||
color: var(--muted);
|
||||
background: var(--paper);
|
||||
padding: 3px 9px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .list-wrap {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .list-wrap::before,
|
||||
.mobile-column-actions-dialog .list-wrap::after {
|
||||
content: "";
|
||||
position: sticky;
|
||||
display: block;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 20px;
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .list-wrap::before {
|
||||
top: 0;
|
||||
background: linear-gradient(to bottom, rgba(255,255,255,0.9), transparent);
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .list-wrap::after {
|
||||
bottom: 0;
|
||||
background: linear-gradient(to top, rgba(255,255,255,0.9), transparent);
|
||||
margin-top: -20px;
|
||||
}
|
||||
|
||||
.mobile-column-top-actions {
|
||||
padding: 10px 24px 0;
|
||||
}
|
||||
|
||||
.mobile-column-top-action {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mobile-column-section {
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .col-header {
|
||||
width: 100%;
|
||||
padding: 12px 24px;
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mobile-column-header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.mobile-column-name {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.mobile-column-meta {
|
||||
color: var(--muted);
|
||||
font-size: 0.78em;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.mobile-column-chevron {
|
||||
color: var(--muted);
|
||||
transition: transform 0.2s ease-out;
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .col-header[aria-expanded="true"] .mobile-column-chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .col-actions[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .col-actions ul,
|
||||
.mobile-column-actions-dialog .col-actions li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .col-actions a,
|
||||
.mobile-column-actions-dialog .col-actions button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px 24px 10px 40px;
|
||||
color: var(--ink);
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
text-decoration: none;
|
||||
background: none;
|
||||
border: 0;
|
||||
border-top: 1px solid #f5f5f5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .col-actions a:hover,
|
||||
.mobile-column-actions-dialog .col-actions button:hover {
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .col-actions a:active,
|
||||
.mobile-column-actions-dialog .col-actions button:active {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.mobile-column-description,
|
||||
.mobile-column-no-actions {
|
||||
margin: 0;
|
||||
padding: 0 24px 12px 24px;
|
||||
color: var(--muted);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .modal-footer {
|
||||
padding: 14px 20px;
|
||||
border-top: 1px solid var(--rule);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .footer-info {
|
||||
flex: 1;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.68rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .btn {
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 9px 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
font-family: inherit;
|
||||
transition: background 0.12s;
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .btn-ghost:hover {
|
||||
background: var(--rule);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
dialog.mobile-column-actions-dialog {
|
||||
width: 95vw;
|
||||
max-height: 85vh;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .modal-header {
|
||||
padding: 16px 18px 14px;
|
||||
}
|
||||
|
||||
.mobile-column-top-actions {
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .col-header {
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.mobile-column-actions-dialog .col-actions a,
|
||||
.mobile-column-actions-dialog .col-actions button {
|
||||
padding-left: 34px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.mobile-column-description,
|
||||
.mobile-column-no-actions {
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
|
||||
.small-screen-only {
|
||||
|
|
@ -803,16 +1081,42 @@ p.zero-results {
|
|||
.filters input.filter-value {
|
||||
width: 140px;
|
||||
}
|
||||
button.choose-columns-mobile {
|
||||
display: inline-block;
|
||||
button.choose-columns-mobile,
|
||||
button.column-actions-mobile {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem 1rem;
|
||||
margin-bottom: 1em;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.2;
|
||||
font-family: inherit;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
button.column-actions-mobile {
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
button.column-actions-mobile svg {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
button.column-actions-mobile span {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
button.choose-columns-mobile {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
318
datasette/static/mobile-column-actions.js
Normal file
318
datasette/static/mobile-column-actions.js
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
var MOBILE_COLUMN_BREAKPOINT = 576;
|
||||
var MOBILE_COLUMN_DIALOG_ID = "mobile-column-actions-dialog";
|
||||
var MOBILE_COLUMN_DIALOG_TITLE_ID = "mobile-column-actions-title";
|
||||
|
||||
function mobileColumnHeaders(manager) {
|
||||
return Array.from(
|
||||
document.querySelectorAll(manager.selectors.tableHeaders),
|
||||
).filter((th) => th.dataset.column);
|
||||
}
|
||||
|
||||
function mobileColumnMetaText(th) {
|
||||
var parts = [];
|
||||
if (th.dataset.columnType) {
|
||||
parts.push(th.dataset.columnType);
|
||||
}
|
||||
if (th.dataset.isPk === "1") {
|
||||
parts.push("pk");
|
||||
}
|
||||
if (th.dataset.columnNotNull === "1") {
|
||||
parts.push("not null");
|
||||
}
|
||||
return parts.join(", ");
|
||||
}
|
||||
|
||||
function createMobileColumnActionNode(itemConfig, closeDialog) {
|
||||
var actionNode;
|
||||
if (itemConfig.href) {
|
||||
actionNode = document.createElement("a");
|
||||
actionNode.href = itemConfig.href;
|
||||
} else {
|
||||
actionNode = document.createElement("button");
|
||||
actionNode.type = "button";
|
||||
}
|
||||
actionNode.textContent = itemConfig.label;
|
||||
|
||||
if (itemConfig.onClick) {
|
||||
actionNode.addEventListener("click", function (ev) {
|
||||
try {
|
||||
itemConfig.onClick.call(actionNode, ev);
|
||||
} finally {
|
||||
closeDialog({ restoreFocus: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return actionNode;
|
||||
}
|
||||
|
||||
function initMobileColumnActions(manager) {
|
||||
var triggerButton = document.querySelector(".column-actions-mobile");
|
||||
if (!triggerButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!window.URLSearchParams ||
|
||||
!window.HTMLDialogElement ||
|
||||
!manager.columnActions
|
||||
) {
|
||||
triggerButton.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mobileColumnHeaders(manager).length) {
|
||||
triggerButton.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
var dialog = document.createElement("dialog");
|
||||
dialog.className = "mobile-column-actions-dialog";
|
||||
dialog.id = MOBILE_COLUMN_DIALOG_ID;
|
||||
dialog.setAttribute("aria-labelledby", MOBILE_COLUMN_DIALOG_TITLE_ID);
|
||||
dialog.innerHTML = `
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" id="${MOBILE_COLUMN_DIALOG_TITLE_ID}">Column actions</span>
|
||||
<span class="modal-meta"></span>
|
||||
</div>
|
||||
<div class="list-wrap mobile-column-list"></div>
|
||||
<div class="modal-footer">
|
||||
<span class="footer-info">Tap a column to reveal actions.</span>
|
||||
<button type="button" class="btn btn-ghost mobile-column-actions-done">Done</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(dialog);
|
||||
|
||||
triggerButton.setAttribute("aria-haspopup", "dialog");
|
||||
triggerButton.setAttribute("aria-controls", MOBILE_COLUMN_DIALOG_ID);
|
||||
triggerButton.setAttribute("aria-expanded", "false");
|
||||
|
||||
var countEl = dialog.querySelector(".modal-meta");
|
||||
var listWrap = dialog.querySelector(".mobile-column-list");
|
||||
var doneButton = dialog.querySelector(".mobile-column-actions-done");
|
||||
var expandedSectionId = null;
|
||||
var shouldRestoreFocus = true;
|
||||
|
||||
function updateExpandedSection() {
|
||||
Array.from(dialog.querySelectorAll(".col-header")).forEach((button) => {
|
||||
var controlsId = button.getAttribute("aria-controls");
|
||||
var actionList = dialog.querySelector("#" + controlsId);
|
||||
var isExpanded = controlsId === expandedSectionId;
|
||||
button.setAttribute("aria-expanded", isExpanded ? "true" : "false");
|
||||
actionList.hidden = !isExpanded;
|
||||
actionList.classList.toggle("expanded", isExpanded);
|
||||
});
|
||||
}
|
||||
|
||||
function scrollExpandedSectionIntoView(section) {
|
||||
var sectionTop = section.offsetTop;
|
||||
var sectionBottom = sectionTop + section.offsetHeight;
|
||||
var visibleTop = listWrap.scrollTop;
|
||||
var visibleBottom = visibleTop + listWrap.clientHeight;
|
||||
var sectionHeight = section.offsetHeight;
|
||||
|
||||
if (sectionTop < visibleTop) {
|
||||
listWrap.scrollTop = sectionTop;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionBottom <= visibleBottom) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionHeight <= listWrap.clientHeight) {
|
||||
listWrap.scrollTop = sectionBottom - listWrap.clientHeight;
|
||||
} else {
|
||||
listWrap.scrollTop = sectionTop;
|
||||
}
|
||||
}
|
||||
|
||||
function closeDialog(options) {
|
||||
options = options || {};
|
||||
shouldRestoreFocus = options.restoreFocus !== false;
|
||||
if (dialog.open) {
|
||||
dialog.close();
|
||||
} else {
|
||||
triggerButton.setAttribute("aria-expanded", "false");
|
||||
if (shouldRestoreFocus) {
|
||||
triggerButton.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderDialog() {
|
||||
var headers = mobileColumnHeaders(manager);
|
||||
if (!headers.length) {
|
||||
closeDialog({ restoreFocus: false });
|
||||
triggerButton.style.display = "none";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!headers.some(
|
||||
(_th, index) => `mobile-column-actions-${index}` === expandedSectionId,
|
||||
)
|
||||
) {
|
||||
expandedSectionId = null;
|
||||
}
|
||||
|
||||
countEl.textContent = `${headers.length} column${
|
||||
headers.length === 1 ? "" : "s"
|
||||
}`;
|
||||
listWrap.innerHTML = "";
|
||||
|
||||
if (manager.columnActions.shouldShowShowAllColumns()) {
|
||||
var topActions = document.createElement("div");
|
||||
topActions.className = "mobile-column-top-actions";
|
||||
|
||||
var showAllColumns = document.createElement("a");
|
||||
showAllColumns.className = "btn btn-ghost mobile-column-top-action";
|
||||
showAllColumns.href = manager.columnActions.showAllColumnsUrl();
|
||||
showAllColumns.textContent = "Show all columns";
|
||||
|
||||
topActions.appendChild(showAllColumns);
|
||||
listWrap.appendChild(topActions);
|
||||
}
|
||||
|
||||
headers.forEach((th, index) => {
|
||||
var sectionId = `mobile-column-actions-${index}`;
|
||||
var actionState = manager.columnActions.buildColumnActionState(th, {
|
||||
includeChooseColumns: false,
|
||||
includeShowAllColumns: false,
|
||||
});
|
||||
var section = document.createElement("section");
|
||||
section.className = "mobile-column-section";
|
||||
|
||||
var headerButton = document.createElement("button");
|
||||
headerButton.type = "button";
|
||||
headerButton.className = "col-header";
|
||||
headerButton.setAttribute("aria-controls", sectionId);
|
||||
headerButton.setAttribute("aria-expanded", "false");
|
||||
|
||||
var headerText = document.createElement("span");
|
||||
headerText.className = "mobile-column-header-text";
|
||||
|
||||
var name = document.createElement("span");
|
||||
name.className = "mobile-column-name";
|
||||
name.textContent = th.dataset.column;
|
||||
headerText.appendChild(name);
|
||||
|
||||
var metaText = mobileColumnMetaText(th);
|
||||
if (metaText) {
|
||||
var meta = document.createElement("span");
|
||||
meta.className = "mobile-column-meta";
|
||||
meta.textContent = metaText;
|
||||
headerText.appendChild(meta);
|
||||
}
|
||||
|
||||
var chevron = document.createElement("span");
|
||||
chevron.className = "mobile-column-chevron";
|
||||
chevron.setAttribute("aria-hidden", "true");
|
||||
chevron.textContent = "▾";
|
||||
|
||||
headerButton.appendChild(headerText);
|
||||
headerButton.appendChild(chevron);
|
||||
headerButton.addEventListener("click", function () {
|
||||
expandedSectionId = expandedSectionId === sectionId ? null : sectionId;
|
||||
updateExpandedSection();
|
||||
if (expandedSectionId === sectionId) {
|
||||
scrollExpandedSectionIntoView(section);
|
||||
}
|
||||
});
|
||||
|
||||
var actionContainer = document.createElement("div");
|
||||
actionContainer.id = sectionId;
|
||||
actionContainer.className = "col-actions";
|
||||
actionContainer.hidden = true;
|
||||
|
||||
if (actionState.columnDescription) {
|
||||
var description = document.createElement("p");
|
||||
description.className = "mobile-column-description";
|
||||
description.textContent = actionState.columnDescription;
|
||||
actionContainer.appendChild(description);
|
||||
}
|
||||
|
||||
if (actionState.actionItems.length) {
|
||||
var actionList = document.createElement("ul");
|
||||
actionState.actionItems.forEach((itemConfig) => {
|
||||
var actionItem = document.createElement("li");
|
||||
actionItem.appendChild(
|
||||
createMobileColumnActionNode(itemConfig, closeDialog),
|
||||
);
|
||||
actionList.appendChild(actionItem);
|
||||
});
|
||||
actionContainer.appendChild(actionList);
|
||||
} else {
|
||||
var noActions = document.createElement("p");
|
||||
noActions.className = "mobile-column-no-actions";
|
||||
noActions.textContent = "No actions available";
|
||||
actionContainer.appendChild(noActions);
|
||||
}
|
||||
|
||||
section.appendChild(headerButton);
|
||||
section.appendChild(actionContainer);
|
||||
listWrap.appendChild(section);
|
||||
});
|
||||
|
||||
updateExpandedSection();
|
||||
return true;
|
||||
}
|
||||
|
||||
function openDialog() {
|
||||
if (window.innerWidth > MOBILE_COLUMN_BREAKPOINT) {
|
||||
return;
|
||||
}
|
||||
if (!renderDialog()) {
|
||||
return;
|
||||
}
|
||||
if (!dialog.open) {
|
||||
dialog.showModal();
|
||||
}
|
||||
triggerButton.setAttribute("aria-expanded", "true");
|
||||
var focusTarget =
|
||||
dialog.querySelector(".mobile-column-top-action") ||
|
||||
dialog.querySelector(".col-header") ||
|
||||
doneButton;
|
||||
focusTarget.focus();
|
||||
}
|
||||
|
||||
triggerButton.addEventListener("click", function () {
|
||||
if (dialog.open) {
|
||||
closeDialog();
|
||||
} else {
|
||||
openDialog();
|
||||
}
|
||||
});
|
||||
|
||||
doneButton.addEventListener("click", function () {
|
||||
closeDialog();
|
||||
});
|
||||
|
||||
dialog.addEventListener("click", function (ev) {
|
||||
if (ev.target === dialog) {
|
||||
closeDialog();
|
||||
}
|
||||
});
|
||||
|
||||
dialog.addEventListener("cancel", function (ev) {
|
||||
ev.preventDefault();
|
||||
closeDialog();
|
||||
});
|
||||
|
||||
dialog.addEventListener("close", function () {
|
||||
triggerButton.setAttribute("aria-expanded", "false");
|
||||
if (shouldRestoreFocus) {
|
||||
triggerButton.focus();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("resize", function () {
|
||||
if (window.innerWidth > MOBILE_COLUMN_BREAKPOINT && dialog.open) {
|
||||
closeDialog({ restoreFocus: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("datasette_init", function (evt) {
|
||||
initMobileColumnActions(evt.detail);
|
||||
});
|
||||
|
|
@ -1,14 +1,6 @@
|
|||
var DROPDOWN_HTML = `<div class="dropdown-menu">
|
||||
<div class="hook"></div>
|
||||
<ul>
|
||||
<li><a class="dropdown-sort-asc" href="#">Sort ascending</a></li>
|
||||
<li><a class="dropdown-sort-desc" href="#">Sort descending</a></li>
|
||||
<li><a class="dropdown-facet" href="#">Facet by this</a></li>
|
||||
<li><a class="dropdown-choose-columns" href="#">Choose columns</a></li>
|
||||
<li><a class="dropdown-hide-column" href="#">Hide this column</a></li>
|
||||
<li><a class="dropdown-show-all-columns" href="#">Show all columns</a></li>
|
||||
<li><a class="dropdown-not-blank" href="#">Show not-blank rows</a></li>
|
||||
</ul>
|
||||
<ul class="dropdown-actions"></ul>
|
||||
<p class="dropdown-column-type"></p>
|
||||
<p class="dropdown-column-description"></p>
|
||||
</div>`;
|
||||
|
|
@ -18,54 +10,230 @@ var DROPDOWN_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" heig
|
|||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||
</svg>`;
|
||||
|
||||
function getParams() {
|
||||
return new URLSearchParams(location.search);
|
||||
}
|
||||
|
||||
function paramsToUrl(params) {
|
||||
var s = params.toString();
|
||||
return s ? "?" + s : location.pathname;
|
||||
}
|
||||
|
||||
function sortDescUrl(column) {
|
||||
var params = getParams();
|
||||
params.set("_sort_desc", column);
|
||||
params.delete("_sort");
|
||||
params.delete("_next");
|
||||
return paramsToUrl(params);
|
||||
}
|
||||
|
||||
function sortAscUrl(column) {
|
||||
var params = getParams();
|
||||
params.set("_sort", column);
|
||||
params.delete("_sort_desc");
|
||||
params.delete("_next");
|
||||
return paramsToUrl(params);
|
||||
}
|
||||
|
||||
function facetUrl(column) {
|
||||
var params = getParams();
|
||||
params.append("_facet", column);
|
||||
return paramsToUrl(params);
|
||||
}
|
||||
|
||||
function hideColumnUrl(column) {
|
||||
var params = getParams();
|
||||
params.append("_nocol", column);
|
||||
return paramsToUrl(params);
|
||||
}
|
||||
|
||||
function showAllColumnsUrl() {
|
||||
var params = getParams();
|
||||
params.delete("_nocol");
|
||||
params.delete("_col");
|
||||
return paramsToUrl(params);
|
||||
}
|
||||
|
||||
function notBlankUrl(column) {
|
||||
var params = getParams();
|
||||
params.set(`${column}__notblank`, "1");
|
||||
return paramsToUrl(params);
|
||||
}
|
||||
|
||||
function getDisplayedFacets() {
|
||||
return Array.from(document.querySelectorAll(".facet-info")).map(
|
||||
(el) => el.dataset.column,
|
||||
);
|
||||
}
|
||||
|
||||
function getColumnClassName(th) {
|
||||
return Array.from(th.classList).find((className) =>
|
||||
className.startsWith("col-"),
|
||||
);
|
||||
}
|
||||
|
||||
function getColumnCells(th) {
|
||||
var table = th.closest("table");
|
||||
var columnClassName = getColumnClassName(th);
|
||||
if (!table || !columnClassName) {
|
||||
return [];
|
||||
}
|
||||
return Array.from(table.querySelectorAll("td." + columnClassName));
|
||||
}
|
||||
|
||||
function getColumnMeta(th) {
|
||||
return {
|
||||
columnName: th.dataset.column,
|
||||
columnNotNull: th.dataset.columnNotNull === "1",
|
||||
columnType: th.dataset.columnType,
|
||||
isPk: th.dataset.isPk === "1",
|
||||
};
|
||||
}
|
||||
|
||||
function getColumnTypeText(th) {
|
||||
var columnType = th.dataset.columnType;
|
||||
if (!columnType) {
|
||||
return null;
|
||||
}
|
||||
var notNull = th.dataset.columnNotNull === "1" ? " NOT NULL" : "";
|
||||
return `Type: ${columnType.toUpperCase()}${notNull}`;
|
||||
}
|
||||
|
||||
function canChooseColumns() {
|
||||
return !!(
|
||||
document.querySelector("column-chooser") && window._columnChooserData
|
||||
);
|
||||
}
|
||||
|
||||
function shouldShowShowAllColumns() {
|
||||
var params = getParams();
|
||||
return params.getAll("_nocol").length || params.getAll("_col").length;
|
||||
}
|
||||
|
||||
function buildColumnActionItems(manager, th, options) {
|
||||
options = options || {};
|
||||
var params = getParams();
|
||||
var column = th.dataset.column;
|
||||
var columnActions = [];
|
||||
var isSortable = !!th.querySelector("a");
|
||||
var isFirstColumn = th.parentElement.querySelector("th:first-of-type") === th;
|
||||
var isSinglePk =
|
||||
th.dataset.isPk === "1" &&
|
||||
document.querySelectorAll('th[data-is-pk="1"]').length === 1;
|
||||
var hasBlankValues = getColumnCells(th).some(
|
||||
(el) => el.innerText.trim() === "",
|
||||
);
|
||||
|
||||
if (isSortable && params.get("_sort") !== column) {
|
||||
columnActions.push({
|
||||
label: "Sort ascending",
|
||||
href: sortAscUrl(column),
|
||||
});
|
||||
}
|
||||
|
||||
if (isSortable && params.get("_sort_desc") !== column) {
|
||||
columnActions.push({
|
||||
label: "Sort descending",
|
||||
href: sortDescUrl(column),
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
DATASETTE_ALLOW_FACET &&
|
||||
!isFirstColumn &&
|
||||
!getDisplayedFacets().includes(column) &&
|
||||
!isSinglePk
|
||||
) {
|
||||
columnActions.push({
|
||||
label: "Facet by this",
|
||||
href: facetUrl(column),
|
||||
});
|
||||
}
|
||||
|
||||
if (options.includeChooseColumns && canChooseColumns()) {
|
||||
columnActions.push({
|
||||
label: "Choose columns",
|
||||
href: "#",
|
||||
onClick:
|
||||
options.onChooseColumns ||
|
||||
function (ev) {
|
||||
ev.preventDefault();
|
||||
openColumnChooser();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (th.dataset.isPk !== "1") {
|
||||
columnActions.push({
|
||||
label: "Hide this column",
|
||||
href: hideColumnUrl(column),
|
||||
});
|
||||
}
|
||||
|
||||
if (options.includeShowAllColumns && shouldShowShowAllColumns()) {
|
||||
columnActions.push({
|
||||
label: "Show all columns",
|
||||
href: showAllColumnsUrl(),
|
||||
});
|
||||
}
|
||||
|
||||
if (params.get(`${column}__notblank`) !== "1" && hasBlankValues) {
|
||||
columnActions.push({
|
||||
label: "Show not-blank rows",
|
||||
href: notBlankUrl(column),
|
||||
});
|
||||
}
|
||||
|
||||
return columnActions.concat(manager.makeColumnActions(getColumnMeta(th)));
|
||||
}
|
||||
|
||||
function buildColumnActionState(manager, th, options) {
|
||||
return {
|
||||
column: th.dataset.column,
|
||||
columnDescription: th.dataset.columnDescription || null,
|
||||
columnMeta: getColumnMeta(th),
|
||||
columnTypeText: getColumnTypeText(th),
|
||||
actionItems: buildColumnActionItems(manager, th, options),
|
||||
};
|
||||
}
|
||||
|
||||
function initializeColumnActions(manager) {
|
||||
manager.columnActions = {
|
||||
buildColumnActionState: function (th, options) {
|
||||
return buildColumnActionState(manager, th, options);
|
||||
},
|
||||
buildColumnActionItems: function (th, options) {
|
||||
return buildColumnActionItems(manager, th, options);
|
||||
},
|
||||
canChooseColumns: canChooseColumns,
|
||||
facetUrl: facetUrl,
|
||||
getColumnMeta: getColumnMeta,
|
||||
getColumnTypeText: getColumnTypeText,
|
||||
hideColumnUrl: hideColumnUrl,
|
||||
notBlankUrl: notBlankUrl,
|
||||
shouldShowShowAllColumns: shouldShowShowAllColumns,
|
||||
showAllColumnsUrl: showAllColumnsUrl,
|
||||
sortAscUrl: sortAscUrl,
|
||||
sortDescUrl: sortDescUrl,
|
||||
};
|
||||
}
|
||||
|
||||
function renderActionLink(itemConfig) {
|
||||
var newLink = document.createElement("a");
|
||||
newLink.textContent = itemConfig.label;
|
||||
newLink.href = itemConfig.href || "#";
|
||||
if (itemConfig.onClick) {
|
||||
newLink.addEventListener("click", itemConfig.onClick);
|
||||
}
|
||||
return newLink;
|
||||
}
|
||||
|
||||
/** Main initialization function for Datasette Table interactions */
|
||||
const initDatasetteTable = function (manager) {
|
||||
// Feature detection
|
||||
if (!window.URLSearchParams) {
|
||||
return;
|
||||
}
|
||||
function getParams() {
|
||||
return new URLSearchParams(location.search);
|
||||
}
|
||||
function paramsToUrl(params) {
|
||||
var s = params.toString();
|
||||
return s ? "?" + s : location.pathname;
|
||||
}
|
||||
function sortDescUrl(column) {
|
||||
var params = getParams();
|
||||
params.set("_sort_desc", column);
|
||||
params.delete("_sort");
|
||||
params.delete("_next");
|
||||
return paramsToUrl(params);
|
||||
}
|
||||
function sortAscUrl(column) {
|
||||
var params = getParams();
|
||||
params.set("_sort", column);
|
||||
params.delete("_sort_desc");
|
||||
params.delete("_next");
|
||||
return paramsToUrl(params);
|
||||
}
|
||||
function facetUrl(column) {
|
||||
var params = getParams();
|
||||
params.append("_facet", column);
|
||||
return paramsToUrl(params);
|
||||
}
|
||||
function hideColumnUrl(column) {
|
||||
var params = getParams();
|
||||
params.append("_nocol", column);
|
||||
return paramsToUrl(params);
|
||||
}
|
||||
function showAllColumnsUrl() {
|
||||
var params = getParams();
|
||||
params.delete("_nocol");
|
||||
params.delete("_col");
|
||||
return paramsToUrl(params);
|
||||
}
|
||||
function notBlankUrl(column) {
|
||||
var params = getParams();
|
||||
params.set(`${column}__notblank`, "1");
|
||||
return paramsToUrl(params);
|
||||
}
|
||||
function closeMenu() {
|
||||
menu.style.display = "none";
|
||||
menu.classList.remove("anim-scale-in");
|
||||
|
|
@ -97,100 +265,34 @@ const initDatasetteTable = function (manager) {
|
|||
var rect = th.getBoundingClientRect();
|
||||
var menuTop = rect.bottom + window.scrollY;
|
||||
var menuLeft = rect.left + window.scrollX;
|
||||
var column = th.getAttribute("data-column");
|
||||
var params = getParams();
|
||||
var sort = menu.querySelector("a.dropdown-sort-asc");
|
||||
var sortDesc = menu.querySelector("a.dropdown-sort-desc");
|
||||
var facetItem = menu.querySelector("a.dropdown-facet");
|
||||
var notBlank = menu.querySelector("a.dropdown-not-blank");
|
||||
var hideColumn = menu.querySelector("a.dropdown-hide-column");
|
||||
var showAllColumns = menu.querySelector("a.dropdown-show-all-columns");
|
||||
var selectColumns = menu.querySelector("a.dropdown-choose-columns");
|
||||
if (params.get("_sort") == column) {
|
||||
sort.parentNode.style.display = "none";
|
||||
} else {
|
||||
sort.parentNode.style.display = "block";
|
||||
sort.setAttribute("href", sortAscUrl(column));
|
||||
}
|
||||
if (params.get("_sort_desc") == column) {
|
||||
sortDesc.parentNode.style.display = "none";
|
||||
} else {
|
||||
sortDesc.parentNode.style.display = "block";
|
||||
sortDesc.setAttribute("href", sortDescUrl(column));
|
||||
}
|
||||
/* Show hide columns options */
|
||||
if (params.get("_nocol") || params.get("_col")) {
|
||||
showAllColumns.parentNode.style.display = "block";
|
||||
showAllColumns.setAttribute("href", showAllColumnsUrl());
|
||||
} else {
|
||||
showAllColumns.parentNode.style.display = "none";
|
||||
}
|
||||
if (th.getAttribute("data-is-pk") != "1") {
|
||||
hideColumn.parentNode.style.display = "block";
|
||||
hideColumn.setAttribute("href", hideColumnUrl(column));
|
||||
} else {
|
||||
hideColumn.parentNode.style.display = "none";
|
||||
}
|
||||
/* Choose columns - show if web component exists */
|
||||
var columnChooser = document.querySelector("column-chooser");
|
||||
if (columnChooser && window._columnChooserData) {
|
||||
selectColumns.parentNode.style.display = "block";
|
||||
selectColumns.addEventListener("click", function (ev) {
|
||||
var actionState = manager.columnActions.buildColumnActionState(th, {
|
||||
includeChooseColumns: true,
|
||||
includeShowAllColumns: true,
|
||||
onChooseColumns: function (ev) {
|
||||
ev.preventDefault();
|
||||
closeMenu();
|
||||
openColumnChooser();
|
||||
});
|
||||
} else {
|
||||
selectColumns.parentNode.style.display = "none";
|
||||
}
|
||||
/* Only show "Facet by this" if it's not the first column, not selected,
|
||||
not a single PK and the Datasette allow_facet setting is True */
|
||||
var displayedFacets = Array.from(
|
||||
document.querySelectorAll(".facet-info"),
|
||||
).map((el) => el.dataset.column);
|
||||
var isFirstColumn =
|
||||
th.parentElement.querySelector("th:first-of-type") == th;
|
||||
var isSinglePk =
|
||||
th.getAttribute("data-is-pk") == "1" &&
|
||||
document.querySelectorAll('th[data-is-pk="1"]').length == 1;
|
||||
if (
|
||||
!DATASETTE_ALLOW_FACET ||
|
||||
isFirstColumn ||
|
||||
displayedFacets.includes(column) ||
|
||||
isSinglePk
|
||||
) {
|
||||
facetItem.parentNode.style.display = "none";
|
||||
} else {
|
||||
facetItem.parentNode.style.display = "block";
|
||||
facetItem.setAttribute("href", facetUrl(column));
|
||||
}
|
||||
/* Show notBlank option if not selected AND at least one visible blank value */
|
||||
var tdsForThisColumn = Array.from(
|
||||
th.closest("table").querySelectorAll("td." + th.className),
|
||||
);
|
||||
if (
|
||||
params.get(`${column}__notblank`) != "1" &&
|
||||
tdsForThisColumn.filter((el) => el.innerText.trim() == "").length
|
||||
) {
|
||||
notBlank.parentNode.style.display = "block";
|
||||
notBlank.setAttribute("href", notBlankUrl(column));
|
||||
} else {
|
||||
notBlank.parentNode.style.display = "none";
|
||||
}
|
||||
var columnTypeP = menu.querySelector(".dropdown-column-type");
|
||||
var columnType = th.dataset.columnType;
|
||||
var notNull = th.dataset.columnNotNull == 1 ? " NOT NULL" : "";
|
||||
},
|
||||
});
|
||||
var menuList = menu.querySelector("ul.dropdown-actions");
|
||||
menuList.innerHTML = "";
|
||||
actionState.actionItems.forEach((itemConfig) => {
|
||||
var menuItem = document.createElement("li");
|
||||
menuItem.appendChild(renderActionLink(itemConfig));
|
||||
menuList.appendChild(menuItem);
|
||||
});
|
||||
|
||||
if (columnType) {
|
||||
var columnTypeP = menu.querySelector(".dropdown-column-type");
|
||||
if (actionState.columnTypeText) {
|
||||
columnTypeP.style.display = "block";
|
||||
columnTypeP.innerText = `Type: ${columnType.toUpperCase()}${notNull}`;
|
||||
columnTypeP.innerText = actionState.columnTypeText;
|
||||
} else {
|
||||
columnTypeP.style.display = "none";
|
||||
}
|
||||
|
||||
var columnDescriptionP = menu.querySelector(".dropdown-column-description");
|
||||
if (th.dataset.columnDescription) {
|
||||
columnDescriptionP.innerText = th.dataset.columnDescription;
|
||||
if (actionState.columnDescription) {
|
||||
columnDescriptionP.innerText = actionState.columnDescription;
|
||||
columnDescriptionP.style.display = "block";
|
||||
} else {
|
||||
columnDescriptionP.style.display = "none";
|
||||
|
|
@ -201,39 +303,6 @@ const initDatasetteTable = function (manager) {
|
|||
menu.style.display = "block";
|
||||
menu.classList.add("anim-scale-in");
|
||||
|
||||
// Custom menu items on each render
|
||||
// Plugin hook: allow adding JS-based additional menu items
|
||||
const columnActionsPayload = {
|
||||
columnName: th.dataset.column,
|
||||
columnNotNull: th.dataset.columnNotNull === "1",
|
||||
columnType: th.dataset.columnType,
|
||||
isPk: th.dataset.isPk === "1",
|
||||
};
|
||||
const columnItemConfigs = manager.makeColumnActions(columnActionsPayload);
|
||||
|
||||
const menuList = menu.querySelector("ul");
|
||||
columnItemConfigs.forEach((itemConfig) => {
|
||||
// Remove items from previous render. We assume entries have unique labels.
|
||||
const existingItems = menuList.querySelectorAll(`li`);
|
||||
Array.from(existingItems)
|
||||
.filter((item) => item.innerText === itemConfig.label)
|
||||
.forEach((node) => {
|
||||
node.remove();
|
||||
});
|
||||
|
||||
const newLink = document.createElement("a");
|
||||
newLink.textContent = itemConfig.label;
|
||||
newLink.href = itemConfig.href ?? "#";
|
||||
if (itemConfig.onClick) {
|
||||
newLink.onclick = itemConfig.onClick;
|
||||
}
|
||||
|
||||
// Attach new elements to DOM
|
||||
const menuItem = document.createElement("li");
|
||||
menuItem.appendChild(newLink);
|
||||
menuList.appendChild(menuItem);
|
||||
});
|
||||
|
||||
// Measure width of menu and adjust position if too far right
|
||||
const menuWidth = menu.offsetWidth;
|
||||
const windowWidth = window.innerWidth;
|
||||
|
|
@ -383,7 +452,7 @@ function openColumnChooser() {
|
|||
}
|
||||
var qs = params.toString();
|
||||
location.href = qs ? "?" + qs : location.pathname;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -391,11 +460,12 @@ function openColumnChooser() {
|
|||
document.addEventListener("datasette_init", function (evt) {
|
||||
const { detail: manager } = evt;
|
||||
|
||||
initializeColumnActions(manager);
|
||||
|
||||
// Main table
|
||||
initDatasetteTable(manager);
|
||||
|
||||
// Other UI functions with interactive JS needs
|
||||
addButtonsToFilterRows(manager);
|
||||
initAutocompleteForFilterValues(manager);
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
{{- super() -}}
|
||||
<script src="{{ urls.static('column-chooser.js') }}" defer></script>
|
||||
<script src="{{ urls.static('table.js') }}" defer></script>
|
||||
<script src="{{ urls.static('mobile-column-actions.js') }}" defer></script>
|
||||
<script>DATASETTE_ALLOW_FACET = {{ datasette_allow_facet }};</script>
|
||||
<style>
|
||||
@media only screen and (max-width: 576px) {
|
||||
|
|
@ -140,6 +141,15 @@
|
|||
{% if all_columns %}
|
||||
<column-chooser></column-chooser>
|
||||
<button class="choose-columns-mobile small-screen-only" onclick="openColumnChooser()">Choose columns</button>
|
||||
{% if display_rows %}
|
||||
<button type="button" class="column-actions-mobile small-screen-only">
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||
</svg>
|
||||
<span>Column actions</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
<script>
|
||||
window._columnChooserData = {{ {"allColumns": all_columns, "selectedColumns": display_columns|map(attribute='name')|list, "primaryKeys": primary_keys}|tojson }};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -750,6 +750,21 @@ async def test_column_chooser_present(ds_client):
|
|||
assert "primaryKeys" in script_text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mobile_column_actions_present(ds_client):
|
||||
response = await ds_client.get("/fixtures/facetable")
|
||||
assert response.status_code == 200
|
||||
soup = Soup(response.text, "html.parser")
|
||||
button = soup.select_one("button.column-actions-mobile.small-screen-only")
|
||||
assert button is not None
|
||||
assert button.text.strip() == "Column actions"
|
||||
assert button.find("svg") is not None
|
||||
assert any(
|
||||
"mobile-column-actions.js" in (script.get("src") or "")
|
||||
for script in soup.find_all("script")
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_column_chooser_data_reflects_col_filtering(ds_client):
|
||||
response = await ds_client.get("/fixtures/facetable?_col=state&_col=created")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue