feat: more work on UI cleanup
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -249,6 +249,13 @@ defmodule BDS.Desktop.ShellCommands do
|
|||||||
nil -> {:error, %{message: "No active project selected"}}
|
nil -> {:error, %{message: "No active project selected"}}
|
||||||
project -> {:ok, project}
|
project -> {:ok, project}
|
||||||
end
|
end
|
||||||
|
rescue
|
||||||
|
error in [Exqlite.Error] ->
|
||||||
|
if String.contains?(Exception.message(error), "no such table: projects") do
|
||||||
|
{:error, %{message: "Project database is not initialized"}}
|
||||||
|
else
|
||||||
|
reraise error, __STACKTRACE__
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp preview_url(server) do
|
defp preview_url(server) do
|
||||||
|
|||||||
@@ -537,8 +537,33 @@ button {
|
|||||||
|
|
||||||
.tab-close {
|
.tab-close {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
font-size: 11px;
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1;
|
||||||
color: var(--vscode-descriptionForeground);
|
color: var(--vscode-descriptionForeground);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close:hover {
|
||||||
|
background-color: var(--vscode-toolbar-hoverBackground);
|
||||||
|
color: var(--vscode-tab-activeForeground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-item-details {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
color: inherit;
|
||||||
|
font: 11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-shell {
|
.editor-shell {
|
||||||
@@ -936,16 +961,31 @@ button {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.panel-tab {
|
.panel-tab {
|
||||||
padding: 8px 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--muted);
|
border: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vscode-tab-inactiveForeground);
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-tab:hover {
|
||||||
|
color: var(--vscode-tab-activeForeground);
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-tab.active {
|
.panel-tab.active {
|
||||||
background: var(--accent-soft);
|
color: var(--vscode-tab-activeForeground);
|
||||||
color: var(--ink);
|
border-bottom-color: var(--vscode-focusBorder);
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-content {
|
.assistant-content {
|
||||||
@@ -956,29 +996,42 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status-bar {
|
.status-bar {
|
||||||
height: 34px;
|
height: 22px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
background-color: var(--vscode-statusBar-background);
|
||||||
padding: 0 12px;
|
color: var(--vscode-statusBar-foreground);
|
||||||
background: var(--status);
|
font-size: 12px;
|
||||||
border-top: 1px solid var(--line);
|
padding: 0 8px;
|
||||||
|
user-select: none;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 0;
|
||||||
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-bar-left,
|
.status-bar-left,
|
||||||
.status-bar-right {
|
.status-bar-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-bar-item {
|
.status-bar-item {
|
||||||
max-width: 180px;
|
display: flex;
|
||||||
padding: 4px 8px;
|
align-items: center;
|
||||||
border-radius: 999px;
|
gap: 6px;
|
||||||
background: rgba(255, 255, 255, 0.06);
|
padding: 0 8px;
|
||||||
font-size: 11px;
|
height: 100%;
|
||||||
|
max-width: none;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ function renderTitlebar() {
|
|||||||
<span class="window-titlebar-panel-pane"></span>
|
<span class="window-titlebar-panel-pane"></span>
|
||||||
</span>
|
</span>
|
||||||
`)}
|
`)}
|
||||||
${renderTitlebarAction("toggle-assistant", "toggle-assistant", "Toggle assistant", `
|
${renderTitlebarAction("toggle-assistant-sidebar", "toggle-assistant", "Toggle assistant", `
|
||||||
<span class="window-titlebar-assistant-icon ${state.session.assistant_sidebar_visible ? "is-active" : "is-inactive"}">
|
<span class="window-titlebar-assistant-icon ${state.session.assistant_sidebar_visible ? "is-active" : "is-inactive"}">
|
||||||
<span class="window-titlebar-assistant-pane"></span>
|
<span class="window-titlebar-assistant-pane"></span>
|
||||||
</span>
|
</span>
|
||||||
@@ -184,7 +184,7 @@ function renderTab(tab) {
|
|||||||
<button class="tab ${active ? "active" : ""} ${tab.is_transient ? "transient" : ""}" data-tab-type="${tab.type}" data-tab-id="${tab.id}" type="button">
|
<button class="tab ${active ? "active" : ""} ${tab.is_transient ? "transient" : ""}" data-tab-type="${tab.type}" data-tab-id="${tab.id}" type="button">
|
||||||
<span class="tab-icon">${tabIcon(tab.type)}</span>
|
<span class="tab-icon">${tabIcon(tab.type)}</span>
|
||||||
<span class="tab-title">${escapeHtml(meta.title)}</span>
|
<span class="tab-title">${escapeHtml(meta.title)}</span>
|
||||||
<span class="tab-close" aria-hidden="true">${tab.is_transient ? "Preview" : "Pinned"}</span>
|
<span class="tab-close" data-close-tab="${tab.type}:${tab.id}" role="button" aria-label="Close ${escapeHtmlAttribute(meta.title)}" title="Close tab">×</span>
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -419,11 +419,13 @@ function applyVisibility() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bindEvents() {
|
function bindEvents() {
|
||||||
root.querySelectorAll("[data-command]").forEach((button) => {
|
root.querySelectorAll("button[data-command]").forEach((button) => {
|
||||||
button.onclick = () => {
|
button.onclick = () => {
|
||||||
const command = button.dataset.command;
|
const command = button.dataset.command;
|
||||||
if (command === "open-tasks-panel") {
|
if (command === "open-tasks-panel") {
|
||||||
openTasksPanel();
|
openTasksPanel();
|
||||||
|
render();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (command === "toggle-offline-mode") {
|
if (command === "toggle-offline-mode") {
|
||||||
executeShellCommand("toggle_offline_mode");
|
executeShellCommand("toggle_offline_mode");
|
||||||
@@ -471,6 +473,15 @@ function bindEvents() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
root.querySelectorAll("[data-close-tab]").forEach((button) => {
|
||||||
|
button.onclick = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
const [type, id] = button.dataset.closeTab.split(":");
|
||||||
|
closeSpecificTab(type, id);
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
root.querySelectorAll("[data-panel-tab]").forEach((button) => {
|
root.querySelectorAll("[data-panel-tab]").forEach((button) => {
|
||||||
button.onclick = () => {
|
button.onclick = () => {
|
||||||
state.session.panel.active_tab = button.dataset.panelTab;
|
state.session.panel.active_tab = button.dataset.panelTab;
|
||||||
@@ -790,6 +801,30 @@ function closeActiveTab() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeSpecificTab(type, id) {
|
||||||
|
const index = state.session.tabs.findIndex((tab) => tab.type === type && tab.id === id);
|
||||||
|
|
||||||
|
if (index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wasActive = state.session.active_tab?.type === type && state.session.active_tab?.id === id;
|
||||||
|
state.session.tabs.splice(index, 1);
|
||||||
|
delete state.tabMeta[`${type}:${id}`];
|
||||||
|
|
||||||
|
if (!state.session.tabs.length) {
|
||||||
|
state.session.active_tab = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wasActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = state.session.tabs[Math.min(index, state.session.tabs.length - 1)];
|
||||||
|
state.session.active_tab = { type: next.type, id: next.id };
|
||||||
|
}
|
||||||
|
|
||||||
function openTab(type, id, title, transient, meta = {}) {
|
function openTab(type, id, title, transient, meta = {}) {
|
||||||
const existingIndex = state.session.tabs.findIndex((tab) => tab.type === type && tab.id === id);
|
const existingIndex = state.session.tabs.findIndex((tab) => tab.type === type && tab.id === id);
|
||||||
|
|
||||||
@@ -1187,7 +1222,7 @@ function renderLanguageOptions() {
|
|||||||
return state.supportedUiLanguages
|
return state.supportedUiLanguages
|
||||||
.map((language) => {
|
.map((language) => {
|
||||||
const selected = language.code === state.uiLanguage ? " selected" : "";
|
const selected = language.code === state.uiLanguage ? " selected" : "";
|
||||||
return `<option value="${escapeHtmlAttribute(language.code)}"${selected}>${escapeHtml(language.code.toUpperCase())}</option>`;
|
return `<option value="${escapeHtmlAttribute(language.code)}"${selected}>${escapeHtml(language.flag || language.code.toUpperCase())}</option>`;
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ for await (const line of rl) {
|
|||||||
window_title: text("[data-testid='window-title']"),
|
window_title: text("[data-testid='window-title']"),
|
||||||
active_view: document.querySelector("[data-testid='activity-button'][data-active='true']")?.dataset.view ?? null,
|
active_view: document.querySelector("[data-testid='activity-button'][data-active='true']")?.dataset.view ?? null,
|
||||||
sidebar_visible: !hasClass("[data-testid='sidebar-shell']", "is-hidden"),
|
sidebar_visible: !hasClass("[data-testid='sidebar-shell']", "is-hidden"),
|
||||||
|
assistant_visible: !hasClass("[data-testid='assistant-shell']", "is-hidden"),
|
||||||
|
panel_visible: !hasClass(".panel-shell", "is-hidden"),
|
||||||
editor_title: text("[data-testid='editor-title']"),
|
editor_title: text("[data-testid='editor-title']"),
|
||||||
activity_labels: texts("[data-testid='activity-button']", (node) => node.getAttribute("aria-label")),
|
activity_labels: texts("[data-testid='activity-button']", (node) => node.getAttribute("aria-label")),
|
||||||
sidebar_sections: texts("[data-testid='sidebar-section-title']", (node) => node.textContent.trim()),
|
sidebar_sections: texts("[data-testid='sidebar-section-title']", (node) => node.textContent.trim()),
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ defmodule BDS.Desktop.AutomationTest do
|
|||||||
assert snapshot.window_title == "Blogging Desktop Server"
|
assert snapshot.window_title == "Blogging Desktop Server"
|
||||||
assert snapshot.active_view == "posts"
|
assert snapshot.active_view == "posts"
|
||||||
assert snapshot.sidebar_visible == true
|
assert snapshot.sidebar_visible == true
|
||||||
|
assert snapshot.assistant_visible == false
|
||||||
|
assert snapshot.panel_visible == false
|
||||||
assert snapshot.editor_title == "Dashboard"
|
assert snapshot.editor_title == "Dashboard"
|
||||||
assert snapshot.activity_labels == [
|
assert snapshot.activity_labels == [
|
||||||
"Posts",
|
"Posts",
|
||||||
@@ -43,6 +45,12 @@ defmodule BDS.Desktop.AutomationTest do
|
|||||||
snapshot = Automation.snapshot(session)
|
snapshot = Automation.snapshot(session)
|
||||||
assert snapshot.sidebar_visible == false
|
assert snapshot.sidebar_visible == false
|
||||||
|
|
||||||
|
assert :ok = Automation.click(session, "[data-testid='toggle-assistant']")
|
||||||
|
|
||||||
|
snapshot = Automation.snapshot(session)
|
||||||
|
assert snapshot.assistant_visible == true
|
||||||
|
assert snapshot.panel_visible == false
|
||||||
|
|
||||||
screenshot_path = Path.join(screenshot_dir, "main-window.png")
|
screenshot_path = Path.join(screenshot_dir, "main-window.png")
|
||||||
assert Automation.capture_screenshot(session, screenshot_path) == screenshot_path
|
assert Automation.capture_screenshot(session, screenshot_path) == screenshot_path
|
||||||
assert File.exists?(screenshot_path)
|
assert File.exists?(screenshot_path)
|
||||||
|
|||||||
@@ -73,6 +73,13 @@ defmodule BDS.Desktop.ShellCommandsTest do
|
|||||||
assert wait_for_task(result.task_id, &(&1.status in [:completed, :failed])).status == :completed
|
assert wait_for_task(result.task_id, &(&1.status in [:completed, :failed])).status == :completed
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "missing project schema returns a command error instead of raising" do
|
||||||
|
BDS.Repo.query!("DROP TABLE projects", [])
|
||||||
|
|
||||||
|
assert {:error, %{message: message}} = ShellCommands.execute("open_in_browser")
|
||||||
|
assert message =~ "Project database is not initialized"
|
||||||
|
end
|
||||||
|
|
||||||
defp wait_for_task(task_id, matcher, timeout \\ 2_000)
|
defp wait_for_task(task_id, matcher, timeout \\ 2_000)
|
||||||
|
|
||||||
defp wait_for_task(task_id, _matcher, timeout) when timeout <= 0 do
|
defp wait_for_task(task_id, _matcher, timeout) when timeout <= 0 do
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ defmodule BDS.UI.ShellTest do
|
|||||||
assert js =~ "window-titlebar-sidebar-icon"
|
assert js =~ "window-titlebar-sidebar-icon"
|
||||||
assert js =~ "window-titlebar-panel-icon"
|
assert js =~ "window-titlebar-panel-icon"
|
||||||
assert js =~ "window-titlebar-assistant-icon"
|
assert js =~ "window-titlebar-assistant-icon"
|
||||||
|
assert js =~ "toggle-assistant-sidebar"
|
||||||
assert js =~ "activity-bar-top"
|
assert js =~ "activity-bar-top"
|
||||||
assert js =~ "activity-bar-bottom"
|
assert js =~ "activity-bar-bottom"
|
||||||
end
|
end
|
||||||
@@ -148,6 +149,7 @@ defmodule BDS.UI.ShellTest do
|
|||||||
assert css =~ "gap: 4px"
|
assert css =~ "gap: 4px"
|
||||||
assert css =~ "padding: 0 8px"
|
assert css =~ "padding: 0 8px"
|
||||||
assert css =~ "height: 100%"
|
assert css =~ "height: 100%"
|
||||||
|
refute css =~ "background: var(--status)"
|
||||||
assert css =~ ".status-bar-language-select"
|
assert css =~ ".status-bar-language-select"
|
||||||
assert css =~ ".status-bar-item.language-badge"
|
assert css =~ ".status-bar-item.language-badge"
|
||||||
assert css =~ ".status-bar-item.offline-badge"
|
assert css =~ ".status-bar-item.offline-badge"
|
||||||
@@ -166,6 +168,9 @@ defmodule BDS.UI.ShellTest do
|
|||||||
assert js =~ "executeBackendShellCommand"
|
assert js =~ "executeBackendShellCommand"
|
||||||
assert js =~ "applyShellCommandResult"
|
assert js =~ "applyShellCommandResult"
|
||||||
assert js =~ "openTasksPanel"
|
assert js =~ "openTasksPanel"
|
||||||
|
assert js =~ "command === \"open-tasks-panel\")"
|
||||||
|
assert js =~ "openTasksPanel();"
|
||||||
|
assert js =~ "return;"
|
||||||
assert js =~ "No background tasks running"
|
assert js =~ "No background tasks running"
|
||||||
assert js =~ "task-list"
|
assert js =~ "task-list"
|
||||||
assert js =~ "output-list"
|
assert js =~ "output-list"
|
||||||
@@ -189,5 +194,8 @@ defmodule BDS.UI.ShellTest do
|
|||||||
assert js =~ "case \"metadata_diff\""
|
assert js =~ "case \"metadata_diff\""
|
||||||
assert js =~ "case \"regenerate_calendar\""
|
assert js =~ "case \"regenerate_calendar\""
|
||||||
assert js =~ "case \"fill_missing_translations\""
|
assert js =~ "case \"fill_missing_translations\""
|
||||||
|
assert js =~ "root.querySelectorAll(\"button[data-command]\")"
|
||||||
|
assert js =~ "[data-close-tab]"
|
||||||
|
assert js =~ "language.flag"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user