feat(tabs): add new tab and close tab

This commit is contained in:
Yann Amsellem
2025-01-13 17:21:19 +01:00
parent 1f8f5985f3
commit 4eb0cdb259
3 changed files with 160 additions and 62 deletions

View File

@@ -2,7 +2,7 @@
import type { Table } from '$lib/olap-engine';
import { sql } from '@codemirror/lang-sql';
import { Compartment, EditorState } from '@codemirror/state';
import { EditorView, keymap, placeholder } from '@codemirror/view';
import { EditorView, placeholder } from '@codemirror/view';
import { untrack } from 'svelte';
import './codemirror.css';
import { default_extensions, default_keymaps } from './extensions';
@@ -11,11 +11,10 @@
type Props = {
value: string;
onExec?: () => unknown;
tables?: Table[];
};
let { value = $bindable(''), onExec, tables = [] }: Props = $props();
let { value = $bindable(''), tables = [] }: Props = $props();
let container: HTMLDivElement;
let editor_view: EditorView;
@@ -37,16 +36,7 @@
value = update.state.doc.toString();
}
}),
placeholder('Enter a query...'),
keymap.of([
{
key: 'Mod-Enter',
run: () => {
onExec?.();
return true;
}
}
])
placeholder('Enter a query...')
]
});

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import type { SvelteHTMLElements } from 'svelte/elements';
interface Props extends Omit<SvelteHTMLElements['svg'], 'width' | 'height'> {
size?: string | number | null;
}
let { size = 24, ...rest }: Props = $props();
</script>
<svg viewBox="0 0 24 24" aria-hidden="true" width={size} height={size} {...rest} data-name="x-mark">
<path
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>

View File

@@ -9,7 +9,9 @@
import { set_app_context } from '$lib/context';
import Bars3 from '$lib/icons/Bars3.svelte';
import Play from '$lib/icons/Play.svelte';
import Plus from '$lib/icons/Plus.svelte';
import Save from '$lib/icons/Save.svelte';
import XMark from '$lib/icons/XMark.svelte';
import type { Table } from '$lib/olap-engine';
import { engine, type OLAPResponse } from '$lib/olap-engine';
import { history_repository, type HistoryEntry } from '$lib/repositories/history';
@@ -23,19 +25,17 @@
let loading = $state(false);
async function handleExec() {
const query = current_tab.contents;
if (loading || !query) {
return;
}
loading = true;
const query_to_execute = query;
response = await engine.exec(query_to_execute).finally(() => (loading = false));
response = await engine.exec(query).finally(() => (loading = false));
const last = await history_repository.getLast();
if (response && last?.content !== query_to_execute) {
await addHistoryEntry(query_to_execute);
}
if (response && last?.content !== query) await addHistoryEntry(query);
}
let tables = $state.raw<Table[]>([]);
@@ -81,6 +81,8 @@
save_query_modal?.show();
}
}
if (event.key === 'Enter' && event.metaKey) handleExec();
}
async function handleCreateQuery({
@@ -92,8 +94,7 @@
async function handleDeleteQuery(query: Query) {
await query_repository.delete(query.id);
const index = queries.indexOf(query);
queries = queries.slice(0, index).concat(queries.slice(index + 1));
queries = queries.toSpliced(queries.indexOf(query), 1);
}
function handleQueryOpen(_query: Query) {
@@ -104,12 +105,7 @@
async function handleQueryRename(query: Query) {
const updated = await query_repository.update(query);
const index = queries.findIndex((query) => query.id === updated.id);
if (index !== -1) {
queries = queries
.slice(0, index)
.concat(updated)
.concat(queries.slice(index + 1));
}
if (index !== -1) queries = queries.with(index, updated);
}
const context_menu = new ContextMenuState();
@@ -123,12 +119,27 @@
if (!is_mobile) open_drawer = false;
});
let tabs = [
{ id: '', content: 'select 1', name: 'new' },
{ id: '', content: 'select 2', name: 'query1' },
{ id: '', content: 'select 3', name: 'query2' }
];
let selected_tab = $state(0);
interface Tab {
id: string;
contents: string;
name: string;
query_id?: Query['id'];
}
let tabs = $state<Tab[]>([{ id: crypto.randomUUID(), contents: '', name: 'Untitled' }]);
let selected_tab_index = $state(0);
const current_tab = $derived(tabs[selected_tab_index]);
function addNewTab() {
const next_index = tabs.length;
tabs.push({ id: crypto.randomUUID(), name: 'Untitled', contents: '' });
selected_tab_index = next_index;
}
function closeTab(index: number) {
tabs.splice(index, 1);
selected_tab_index = Math.max(0, index - 1);
}
</script>
<svelte:window onkeydown={handleKeyDown} bind:innerWidth={screen_width} />
@@ -169,35 +180,57 @@
<SplitPane type="vertical" min="20%" max="80%" --color="hsl(0deg 0% 12%)">
{#snippet a()}
<div>
<nav class="Tabs">
<div class="left">
<nav class="navigation">
<div class="tabs-container">
{#if is_mobile}
<button onclick={() => (open_drawer = true)}>
<button class="action" onclick={() => (open_drawer = true)}>
<Bars3 size="12" />
</button>
{/if}
<span class="tabs">
{#each tabs as tab, i}
<div
class="tab {i === selected_tab ? 'active' : ''}"
onclick={() => (selected_tab = i)}
{#each tabs as tab, i}
<div
class="tab"
class:active={i === selected_tab_index}
role="button"
onclick={() => (selected_tab_index = i)}
tabindex="0"
onkeyup={() => {}}
>
<span>{tab.name}</span>
<button
class="close"
class:hidden={tabs.length === 1 || i !== selected_tab_index}
onclick={(e) => {
e.stopPropagation();
closeTab(i);
}}
>
<span>{tab.name}</span>
</div>
{/each}
</span>
<XMark size="12" />
</button>
</div>
{/each}
<button
onclick={addNewTab}
class="add-new"
aria-label="Open new tab"
title="Open new tab"
>
<Plus size="14" />
</button>
</div>
<div></div>
<div class="right">
<button onclick={() => save_query_modal?.show()} disabled={!query}>
<div class="workspace-actions">
<button class="action" onclick={() => save_query_modal?.show()} disabled={!query}>
<Save size="12" />
</button>
<button onclick={handleExec} disabled={loading}><Play size="12" /></button>
<button class="action" onclick={handleExec} disabled={loading}>
<Play size="12" />
</button>
</div>
</nav>
{#each tabs as tab, i}
<div style={`display: ${selected_tab == i ? 'block' : 'none'}`}>
<Editor bind:value={tab.content} onExec={handleExec} {tables} />
<div style:display={selected_tab_index == i ? 'block' : 'none'}>
<Editor bind:value={tab.contents} {tables} />
</div>
{/each}
</div>
@@ -213,23 +246,37 @@
<SaveQueryModal bind:this={save_query_modal} onCreate={handleCreateQuery} />
<style>
.Tabs {
.navigation {
height: 28px;
display: flex;
border-bottom: 1px solid hsl(0deg 0% 20%);
& > .left {
flex: 1;
align-items: center;
height: 100%;
position: relative;
&::before {
content: '';
position: absolute;
width: 100%;
height: 1px;
bottom: 0px;
left: 0;
background-color: hsl(0deg 0% 20%);
z-index: 1;
}
& > .right {
& > .tabs-container {
flex: 1;
display: flex;
align-items: center;
height: 100%;
overflow-x: hidden;
}
& > .workspace-actions {
display: flex;
gap: 0px;
}
& button {
& button.action {
background-color: transparent;
border-radius: 0;
}
@@ -240,18 +287,61 @@
}
.tab {
height: 100%;
font-size: 11px;
border-right: 1px solid hsl(0deg 0% 20%);
padding: 0 10px 0 10px;
padding: 0 16px 0 10px;
display: inline-flex;
align-items: center;
height: 100%;
color: hsl(0deg 0% 70%);
position: relative;
&:hover {
cursor: pointer;
}
&.active {
background-color: hsl(0deg 0% 5%);
color: hsl(0deg 0% 100%);
z-index: 2;
}
& > .close {
position: absolute;
display: flex;
place-items: center;
background-color: transparent;
right: 0;
padding: 2px;
&.hidden {
display: none;
}
}
}
.active {
background-color: hsl(0deg 0% 12%);
color: white;
.add-new {
height: calc(100% - 8px);
aspect-ratio: 1;
padding: 4px;
margin-left: 4px;
border-radius: 4px;
&:hover {
cursor: pointer;
background-color: hsl(0deg 0% 17%);
}
&:active {
background-color: hsl(0deg 0% 20%);
}
& :global(svg) {
width: 100%;
height: 100%;
}
}
button {