mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-09-11 12:25:42 +03:00
command palette refactor
This commit is contained in:
parent
f842829583
commit
5fa0b9e68e
@ -1,507 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import FileIcon from './icons/FileIcon.svelte';
|
||||
import CommitIcon from './icons/CommitIcon.svelte';
|
||||
import BranchIcon from './icons/BranchIcon.svelte';
|
||||
import ContactIcon from './icons/ContactIcon.svelte';
|
||||
import ProjectIcon from './icons/ProjectIcon.svelte';
|
||||
import IconPlayerPlayFilled from './icons/IconPlayerPlayFilled.svelte';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { goto } from '$app/navigation';
|
||||
import { shortPath } from '$lib/paths';
|
||||
import { currentProject } from '$lib/current_project';
|
||||
import type { Project } from '$lib/projects';
|
||||
import toast from 'svelte-french-toast';
|
||||
import type { Readable } from 'svelte/store';
|
||||
|
||||
export let projects: Readable<Project[]>;
|
||||
|
||||
let showPalette = <string | false>false;
|
||||
let palette: HTMLElement;
|
||||
|
||||
let changedFiles = {};
|
||||
let commitMessage = '';
|
||||
let commitMessageInput: HTMLElement;
|
||||
let paletteMode = 'command';
|
||||
|
||||
const listFiles = (params: { projectId: string }) =>
|
||||
invoke<Record<string, string>>('git_status', params);
|
||||
|
||||
const matchFiles = (params: { projectId: string; matchPattern: string }) =>
|
||||
invoke<Array<string>>('git_match_paths', params);
|
||||
|
||||
const listBranches = (params: { projectId: string }) =>
|
||||
invoke<Array<string>>('git_branches', params);
|
||||
|
||||
const switchBranch = (params: { projectId: string; branch: string }) =>
|
||||
invoke<Array<string>>('git_switch_branch', params);
|
||||
|
||||
const commit = (params: {
|
||||
projectId: string;
|
||||
message: string;
|
||||
files: Array<string>;
|
||||
push: boolean;
|
||||
}) => invoke<boolean>('git_commit', params);
|
||||
|
||||
const onCmdK = (event: KeyboardEvent) => {
|
||||
if (event.key === 'k') {
|
||||
showPalette = 'command';
|
||||
setTimeout(function () {
|
||||
document.getElementById('command')?.focus();
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
const onKeyD = (event: KeyboardEvent) => {
|
||||
if (event.metaKey) {
|
||||
switch (event.key) {
|
||||
case 'k':
|
||||
console.log('COMMAND');
|
||||
showPalette = 'command';
|
||||
setTimeout(function () {
|
||||
document.getElementById('command')?.focus();
|
||||
}, 100);
|
||||
break;
|
||||
case 'c':
|
||||
showPalette = 'commit';
|
||||
executeCommand('commit');
|
||||
break;
|
||||
case 'e':
|
||||
executeCommand('contact');
|
||||
break;
|
||||
case 'p':
|
||||
showPalette = false;
|
||||
executeCommand('player');
|
||||
break;
|
||||
case 'r':
|
||||
executeCommand('branch');
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (event.key) {
|
||||
case 'Meta':
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'Escape':
|
||||
showPalette = false;
|
||||
paletteMode = 'command';
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (showPalette == 'command') {
|
||||
event.preventDefault();
|
||||
downMenu();
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
if (showPalette == 'command') {
|
||||
event.preventDefault();
|
||||
upMenu();
|
||||
}
|
||||
break;
|
||||
case 'Enter':
|
||||
if (showPalette == 'command') {
|
||||
event.preventDefault();
|
||||
selectItem();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function checkPaletteModal(event: Event) {
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target) {
|
||||
showPalette = false;
|
||||
}
|
||||
if (showPalette !== false && !palette.contains(target)) {
|
||||
showPalette = false;
|
||||
}
|
||||
}
|
||||
|
||||
let activeClass = ['active', 'bg-zinc-700/50', 'text-white'];
|
||||
|
||||
function upMenu() {
|
||||
const menu = document.getElementById('commandMenu');
|
||||
if (menu) {
|
||||
const items = menu.querySelectorAll('li.item');
|
||||
const active = menu.querySelector('li.active');
|
||||
if (active) {
|
||||
const index = Array.from(items).indexOf(active);
|
||||
if (index > 0) {
|
||||
items[index - 1].classList.add(...activeClass);
|
||||
}
|
||||
active.classList.remove(...activeClass);
|
||||
} else {
|
||||
items[items.length - 1].classList.add(...activeClass);
|
||||
}
|
||||
// scroll into view
|
||||
const active2 = menu.querySelector('li.active');
|
||||
if (active2) {
|
||||
active2.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function downMenu() {
|
||||
const menu = document.getElementById('commandMenu');
|
||||
if (menu) {
|
||||
const items = menu.querySelectorAll('li.item');
|
||||
const active = menu.querySelector('li.active');
|
||||
if (active) {
|
||||
const index = Array.from(items).indexOf(active);
|
||||
if (index < items.length - 1) {
|
||||
items[index + 1].classList.add(...activeClass);
|
||||
active.classList.remove(...activeClass);
|
||||
}
|
||||
} else {
|
||||
items[0].classList.add(...activeClass);
|
||||
}
|
||||
// scroll into view
|
||||
const active2 = menu.querySelector('li.active');
|
||||
if (active2) {
|
||||
active2.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectItem() {
|
||||
showPalette = false;
|
||||
const menu = document.getElementById('commandMenu');
|
||||
if (menu) {
|
||||
const active = menu.querySelector('li.active');
|
||||
if (active) {
|
||||
const command = active.getAttribute('data-command');
|
||||
const context = active.getAttribute('data-context');
|
||||
if (command) {
|
||||
executeCommand(command, context);
|
||||
}
|
||||
} else {
|
||||
if ($currentProject) {
|
||||
goto('/projects/' + $currentProject.id + '/search?search=' + search);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function executeCommand(command: string, context?: string | null) {
|
||||
switch (command) {
|
||||
case 'commit':
|
||||
if ($currentProject) {
|
||||
listFiles({ projectId: $currentProject.id }).then((files) => {
|
||||
changedFiles = files;
|
||||
});
|
||||
showPalette = 'commit';
|
||||
setTimeout(function () {
|
||||
commitMessageInput.focus();
|
||||
}, 100);
|
||||
}
|
||||
break;
|
||||
case 'contact':
|
||||
goto('/contact');
|
||||
break;
|
||||
case 'switch':
|
||||
goto('/projects/' + context);
|
||||
break;
|
||||
case 'player':
|
||||
if (context) {
|
||||
goto(`/projects/${$currentProject?.id}/player?file=${encodeURIComponent(context)}`);
|
||||
} else {
|
||||
goto('/projects/' + $currentProject?.id + '/player');
|
||||
}
|
||||
break;
|
||||
case 'branch':
|
||||
showPalette = 'command';
|
||||
branchSwitcher();
|
||||
break;
|
||||
case 'switchBranch':
|
||||
if ($currentProject) {
|
||||
toast.success('Not implelmented yet. :(', {
|
||||
icon: '🛠️'
|
||||
});
|
||||
|
||||
/*
|
||||
this is a little dangerous right now, so lets ice it for a bit
|
||||
switchBranch({ projectId: $currentProject.id, branch: context || '' }).then(() => {
|
||||
});
|
||||
*/
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let search = '';
|
||||
|
||||
$: {
|
||||
searchChanged(search, showPalette == 'command');
|
||||
}
|
||||
|
||||
let projectCommands = [
|
||||
{ text: 'Commit', key: 'C', icon: CommitIcon, command: 'commit' },
|
||||
{ text: 'Player', key: 'P', icon: IconPlayerPlayFilled, command: 'player' },
|
||||
{ text: 'Branch', key: 'R', icon: BranchIcon, command: 'branch' }
|
||||
];
|
||||
|
||||
let switchCommands = [];
|
||||
projects.subscribe((projects) => {
|
||||
switchCommands = [];
|
||||
projects.forEach((p) => {
|
||||
if (p.id !== $currentProject?.id) {
|
||||
switchCommands.push({
|
||||
text: p.title,
|
||||
icon: ProjectIcon,
|
||||
command: 'switch',
|
||||
context: p.id
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let baseCommands = [{ text: 'Contact Us', key: 'E', icon: ContactIcon, command: 'contact' }];
|
||||
|
||||
function commandList() {
|
||||
let commands = [];
|
||||
let divider = [{ type: 'divider' }];
|
||||
if ($currentProject) {
|
||||
commands = projectCommands.concat(divider).concat(switchCommands);
|
||||
} else {
|
||||
commands = switchCommands;
|
||||
}
|
||||
commands = commands.concat(divider).concat(baseCommands);
|
||||
return commands;
|
||||
}
|
||||
|
||||
$: menuItems = commandList();
|
||||
|
||||
function searchChanged(searchValue: string, showCommand: boolean) {
|
||||
if (!showCommand) {
|
||||
search = '';
|
||||
}
|
||||
if (searchValue.length == 0 && paletteMode == 'command') {
|
||||
updateMenu([]);
|
||||
return;
|
||||
}
|
||||
if ($currentProject && searchValue.length > 0) {
|
||||
const searchPattern = '.*' + Array.from(searchValue).join('(.*)');
|
||||
matchFiles({ projectId: $currentProject.id, matchPattern: searchPattern }).then((files) => {
|
||||
let searchResults = [];
|
||||
files.slice(0, 5).forEach((f) => {
|
||||
searchResults.push({ text: f, icon: FileIcon, command: 'player', context: f });
|
||||
});
|
||||
updateMenu(searchResults);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function branchSwitcher() {
|
||||
paletteMode = 'branch';
|
||||
if ($currentProject) {
|
||||
listBranches({ projectId: $currentProject.id }).then((refs) => {
|
||||
let branches: any[] = [];
|
||||
refs.forEach((b) => {
|
||||
branches.push({ text: b, icon: BranchIcon, command: 'switchBranch', context: b });
|
||||
});
|
||||
menuItems = branches;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateMenu(searchResults: Array<{ text: string }>) {
|
||||
if (searchResults.length == 0) {
|
||||
menuItems = commandList();
|
||||
} else {
|
||||
menuItems = searchResults;
|
||||
}
|
||||
}
|
||||
|
||||
function doCommit() {
|
||||
// get checked files
|
||||
let changedFiles: Array<string> = [];
|
||||
let doc = document.getElementsByClassName('file-checkbox');
|
||||
Array.from(doc).forEach((c) => {
|
||||
if (c.checked) {
|
||||
changedFiles.push(c.dataset['file']);
|
||||
}
|
||||
});
|
||||
if ($currentProject) {
|
||||
commit({
|
||||
projectId: $currentProject.id,
|
||||
message: commitMessage,
|
||||
files: changedFiles,
|
||||
push: false
|
||||
}).then((result) => {
|
||||
toast.success('Commit successful!', {
|
||||
icon: '🎉'
|
||||
});
|
||||
commitMessage = '';
|
||||
showPalette = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onCmdK} />
|
||||
<div on:keydown={onKeyD} on:click={checkPaletteModal}>
|
||||
{#if showPalette}
|
||||
<div class="relative z-10" role="dialog" aria-modal="true">
|
||||
<div
|
||||
class="fixed inset-0 bg-zinc-900 bg-opacity-80 transition-opacity"
|
||||
in:fade={{ duration: 50 }}
|
||||
out:fade={{ duration: 50 }}
|
||||
/>
|
||||
|
||||
<div class="command-palette-modal fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||
{#if showPalette == 'command'}
|
||||
<div
|
||||
in:fade={{ duration: 100 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
bind:this={palette}
|
||||
class="mx-auto max-w-2xl transform divide-y divide-zinc-500 divide-opacity-20 overflow-hidden rounded-xl border border-zinc-700 bg-zinc-900 shadow-2xl transition-all"
|
||||
style="
|
||||
height: auto;
|
||||
max-height: 420px;
|
||||
border-width: 0.5px;
|
||||
-webkit-backdrop-filter: blur(20px) saturate(190%) contrast(70%) brightness(80%);
|
||||
backdrop-filter: blur(20px) saturate(190%) contrast(70%) brightness(80%);
|
||||
background-color: rgba(24, 24, 27, 0.60);
|
||||
border: 0.5px solid rgba(63, 63, 70, 0.50);"
|
||||
>
|
||||
{#if paletteMode == 'command'}
|
||||
<div class="relative">
|
||||
<svg
|
||||
class="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-zinc-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
id="command"
|
||||
type="text"
|
||||
bind:value={search}
|
||||
class="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-white focus:ring-0 sm:text-sm"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if paletteMode == 'branch'}
|
||||
<div class="p-4 text-lg">Branch Switcher</div>
|
||||
{/if}
|
||||
|
||||
<!-- Default state, show/hide based on command palette state. -->
|
||||
<ul class="scroll-py-2 divide-y divide-zinc-500 divide-opacity-20 overflow-y-auto">
|
||||
<li class="p-1">
|
||||
<ul id="commandMenu" class="text-sm text-zinc-400">
|
||||
{#each menuItems as item}
|
||||
{#if item.type == 'divider'}
|
||||
<li class="my-2 border-t border-zinc-500 border-opacity-20" />
|
||||
{:else}
|
||||
<!-- Active: "bg-zinc-800 text-white" -->
|
||||
<li
|
||||
class="item group flex cursor-default select-none items-center rounded-md px-3 py-2"
|
||||
on:click={() => {
|
||||
executeCommand(item.command);
|
||||
}}
|
||||
data-command={item.command}
|
||||
data-context={item.context}
|
||||
>
|
||||
<!-- Active: "text-white", Not Active: "text-zinc-500" -->
|
||||
<svelte:component this={item.icon} />
|
||||
<span class="ml-3 flex-auto truncate">{item.text}</span>
|
||||
{#if item.key}
|
||||
<span
|
||||
class="ml-3 flex-none rounded border-b border-black bg-zinc-800 px-1 py-1 text-xs font-semibold text-zinc-400"
|
||||
>
|
||||
<kbd class="font-sans">⌘</kbd><kbd class="font-sans">{item.key}</kbd>
|
||||
</span>
|
||||
{/if}
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showPalette == 'commit'}
|
||||
<div
|
||||
in:fade={{ duration: 100 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
bind:this={palette}
|
||||
class="mx-auto max-w-2xl transform overflow-hidden rounded-xl border border-zinc-700 bg-zinc-900 shadow-2xl transition-all"
|
||||
style="
|
||||
border-width: 0.5px;
|
||||
border: 0.5px solid rgba(63, 63, 70, 0.50);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(190%) contrast(70%) brightness(80%);
|
||||
background-color: rgba(24, 24, 27, 0.6);
|
||||
"
|
||||
>
|
||||
<div class="mb-4 w-full border-b border-zinc-700 p-4 text-lg text-white">
|
||||
Commit Your Changes
|
||||
</div>
|
||||
<div
|
||||
class="relative m-auto transform overflow-hidden p-2 text-left transition-all sm:w-full sm:max-w-sm"
|
||||
>
|
||||
{#if Object.entries(changedFiles).length > 0}
|
||||
<div>
|
||||
<div class="">
|
||||
<h3 class="text-base font-semibold text-zinc-200" id="modal-title">
|
||||
Commit Message
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<div class="mt-2">
|
||||
<textarea
|
||||
rows="4"
|
||||
name="message"
|
||||
placeholder="Description of changes"
|
||||
id="commit-message"
|
||||
bind:this={commitMessageInput}
|
||||
bind:value={commitMessage}
|
||||
class="block w-full rounded-md border-0 p-4 text-zinc-200 ring-1 ring-inset ring-gray-600 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:py-1.5 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 sm:mt-6">
|
||||
<button
|
||||
type="button"
|
||||
on:click={doCommit}
|
||||
class="inline-flex w-full justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>Commit Your Changes</button
|
||||
>
|
||||
</div>
|
||||
<div class="mt-4 py-4 text-zinc-200">
|
||||
<h3 class="text-base font-semibold text-zinc-200" id="modal-title">
|
||||
Changed Files
|
||||
</h3>
|
||||
{#each Object.entries(changedFiles) as file}
|
||||
<div class="flex flex-row space-x-2">
|
||||
<div>
|
||||
<input type="checkbox" class="file-checkbox" data-file={file[0]} checked />
|
||||
</div>
|
||||
<div>
|
||||
{file[1]}
|
||||
</div>
|
||||
<div class="font-mono">
|
||||
{shortPath(file[0])}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mx-auto text-center text-white">No changes to commit</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
24
src/lib/components/CommandPalette/BaseDialog.svelte
Normal file
24
src/lib/components/CommandPalette/BaseDialog.svelte
Normal file
@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { scale } from 'svelte/transition';
|
||||
let dialog: HTMLDialogElement;
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(() => {
|
||||
dialog.showModal();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<dialog
|
||||
class="rounded-lg
|
||||
border border-zinc-400/40
|
||||
bg-zinc-900/70 p-0 shadow-lg
|
||||
backdrop-blur-xl
|
||||
"
|
||||
in:scale={{ duration: 150 }}
|
||||
bind:this={dialog}
|
||||
on:click|self={() => dialog.close()}
|
||||
on:close
|
||||
>
|
||||
<slot />
|
||||
</dialog>
|
7
src/lib/components/CommandPalette/Branch.svelte
Normal file
7
src/lib/components/CommandPalette/Branch.svelte
Normal file
@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import BaseDialog from './BaseDialog.svelte';
|
||||
</script>
|
||||
|
||||
<BaseDialog on:close>
|
||||
<h1>Branch</h1>
|
||||
</BaseDialog>
|
248
src/lib/components/CommandPalette/CmdK.svelte
Normal file
248
src/lib/components/CommandPalette/CmdK.svelte
Normal file
@ -0,0 +1,248 @@
|
||||
<script lang="ts">
|
||||
import BaseDialog from './BaseDialog.svelte';
|
||||
import { currentProject } from '$lib/current_project';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Readable } from 'svelte/store';
|
||||
import type { Project } from '$lib/projects';
|
||||
import type { CommandGroup, Command } from './types';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import tinykeys from 'tinykeys';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Action, previousCommand, nextCommand, firstVisibleCommand } from './types';
|
||||
import Replay from './Replay.svelte';
|
||||
import Commit from './Commit.svelte';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
$: scopeToProject = $currentProject ? true : false;
|
||||
|
||||
let userInput = '';
|
||||
|
||||
const debounce = <T extends (...args: any[]) => any>(fn: T, delay: number) => {
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
return (...args: any[]) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => fn(...args), delay);
|
||||
};
|
||||
};
|
||||
let matchFilesQuery = '';
|
||||
const updateMatchFilesQuery = debounce(async () => {
|
||||
matchFilesQuery = userInput;
|
||||
}, 100);
|
||||
const matchFiles = (params: { projectId: string; matchPattern: string }) =>
|
||||
invoke<Array<string>>('git_match_paths', params);
|
||||
let matchingFiles: Array<string> = [];
|
||||
$: if (matchFilesQuery) {
|
||||
matchFiles({ projectId: $currentProject?.id || '', matchPattern: matchFilesQuery }).then(
|
||||
(files) => {
|
||||
matchingFiles = files;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
matchingFiles = [];
|
||||
}
|
||||
|
||||
let projects: Readable<any> = getContext('projects');
|
||||
|
||||
let selection: [number, number] = [0, 0];
|
||||
// if the group or the command are no longer visible, select the first visible group and first visible command
|
||||
$: if (
|
||||
!commandGroups[selection[0]]?.visible ||
|
||||
!commandGroups[selection[0]].commands[selection[1]]?.visible
|
||||
) {
|
||||
selection = firstVisibleCommand(commandGroups);
|
||||
}
|
||||
$: selectedCommand = commandGroups[selection[0]].commands[selection[1]];
|
||||
$: {
|
||||
const element = document.getElementById(`${selection[0]}-${selection[1]}`);
|
||||
if (element) {
|
||||
// TODO: this works, but it's not standard
|
||||
element.scrollIntoViewIfNeeded(false);
|
||||
}
|
||||
}
|
||||
|
||||
$: commandGroups = [
|
||||
{
|
||||
name: 'Go to project',
|
||||
visible: !scopeToProject,
|
||||
commands: $projects.map((project: Project) => {
|
||||
return {
|
||||
title: project.title,
|
||||
description: 'Repository',
|
||||
selected: false,
|
||||
action: {
|
||||
href: `/projects/${project.id}/`
|
||||
},
|
||||
visible: project.title.toLowerCase().includes(userInput?.toLowerCase())
|
||||
};
|
||||
})
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
visible: scopeToProject,
|
||||
commands: [
|
||||
{
|
||||
title: 'Commit',
|
||||
description: 'C',
|
||||
selected: false,
|
||||
action: {
|
||||
component: Commit
|
||||
},
|
||||
visible: 'commit'.includes(userInput?.toLowerCase())
|
||||
},
|
||||
{
|
||||
title: 'Replay History',
|
||||
description: 'R',
|
||||
selected: false,
|
||||
action: {
|
||||
component: Replay
|
||||
},
|
||||
visible: 'replay history'.includes(userInput?.toLowerCase())
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Files',
|
||||
visible: scopeToProject,
|
||||
description: !userInput
|
||||
? 'type part of a file name'
|
||||
: matchingFiles.length === 0
|
||||
? `no files containing '${userInput}'`
|
||||
: '',
|
||||
commands: matchingFiles.map((file) => {
|
||||
return {
|
||||
title: file,
|
||||
description: 'File',
|
||||
selected: false,
|
||||
action: {
|
||||
href: `/`
|
||||
},
|
||||
visible: true
|
||||
};
|
||||
})
|
||||
}
|
||||
] as CommandGroup[];
|
||||
|
||||
const triggerCommand = () => {
|
||||
// If the selected command is a link, navigate to it, otherwise, emit a 'newdialog' event, handled in the parent component
|
||||
dispatch('newdialog', Commit);
|
||||
if (Action.isLink(selectedCommand.action)) {
|
||||
goto(selectedCommand.action.href);
|
||||
dispatch('close');
|
||||
} else if (Action.isActionInPalette(selectedCommand.action)) {
|
||||
dispatch('newdialog', selectedCommand.action.component);
|
||||
}
|
||||
};
|
||||
|
||||
let unsubscribeKeyboardHandler: () => void;
|
||||
|
||||
onMount(() => {
|
||||
unsubscribeKeyboardHandler = tinykeys(window, {
|
||||
Backspace: () => {
|
||||
if (!userInput) {
|
||||
scopeToProject = false;
|
||||
}
|
||||
},
|
||||
Enter: () => {
|
||||
triggerCommand();
|
||||
},
|
||||
ArrowDown: () => {
|
||||
selection = nextCommand(commandGroups, selection);
|
||||
},
|
||||
ArrowUp: () => {
|
||||
selection = previousCommand(commandGroups, selection);
|
||||
},
|
||||
'Control+n': () => {
|
||||
selection = nextCommand(commandGroups, selection);
|
||||
},
|
||||
'Control+p': () => {
|
||||
selection = previousCommand(commandGroups, selection);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribeKeyboardHandler?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<BaseDialog on:close>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="flex h-[640px] w-[640px] flex-col rounded text-zinc-400" on:click|stopPropagation>
|
||||
<!-- Search input area -->
|
||||
<div class="flex items-center border-b border-zinc-400/20 py-2">
|
||||
<div class="ml-4 mr-2 flex flex-grow items-center">
|
||||
<!-- Project scope -->
|
||||
{#if scopeToProject}
|
||||
<div class="mr-1 flex items-center">
|
||||
<span class="font-semibold text-zinc-300">{$currentProject?.title}</span>
|
||||
<span class="ml-1 text-lg">/</span>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Search input -->
|
||||
<div class="mr-1 flex-grow">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
class="w-full bg-transparent text-zinc-300 focus:outline-none"
|
||||
bind:value={userInput}
|
||||
on:input|stopPropagation={updateMatchFilesQuery}
|
||||
type="text"
|
||||
autofocus
|
||||
placeholder={!scopeToProject
|
||||
? 'Search for repositories'
|
||||
: 'Search for commands, files and code changes...'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Main part -->
|
||||
<div class="flex-auto overflow-y-auto">
|
||||
{#each commandGroups as group, groupIdx}
|
||||
{#if group.visible}
|
||||
<div class="mx-2 cursor-default select-none">
|
||||
<p class="mx-2 cursor-default select-none py-2 text-sm font-semibold text-zinc-300/80">
|
||||
<span>{group.name}</span>
|
||||
{#if group.description}
|
||||
<span class="ml-2 font-light italic text-zinc-300/60">({group.description})</span>
|
||||
{/if}
|
||||
</p>
|
||||
<ul class="">
|
||||
{#each group.commands as command, commandIdx}
|
||||
{#if command.visible}
|
||||
{#if Action.isLink(command.action)}
|
||||
<a
|
||||
on:mouseover={() => (selection = [groupIdx, commandIdx])}
|
||||
on:focus={() => (selection = [groupIdx, commandIdx])}
|
||||
id={`${groupIdx}-${commandIdx}`}
|
||||
href={command.action.href}
|
||||
class="{selection[0] === groupIdx && selection[1] === commandIdx
|
||||
? 'bg-zinc-700/70'
|
||||
: ''} flex cursor-default items-center rounded-lg p-2 px-2 outline-none"
|
||||
>
|
||||
<span class="flex-grow">{command.title}</span>
|
||||
<span>{command.description}</span>
|
||||
</a>
|
||||
{:else if Action.isActionInPalette(command.action)}
|
||||
<div
|
||||
on:mouseover={() => (selection = [groupIdx, commandIdx])}
|
||||
on:focus={() => (selection = [groupIdx, commandIdx])}
|
||||
on:click={triggerCommand}
|
||||
class="{selection[0] === groupIdx && selection[1] === commandIdx
|
||||
? 'bg-zinc-700/70'
|
||||
: ''} flex cursor-default items-center rounded-lg p-2 px-2 outline-none"
|
||||
>
|
||||
<span class="flex-grow">{command.title}</span>
|
||||
<span>{command.description}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
@ -1,315 +1,81 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Readable } from 'svelte/store';
|
||||
import { currentProject } from '$lib/current_project';
|
||||
import { IconCircleCancel } from '$lib/components/icons';
|
||||
import type { Project } from '$lib/projects';
|
||||
import tinykeys from 'tinykeys';
|
||||
import type { CommandGroup, Command } from './commands';
|
||||
import { Action, previousCommand, nextCommand, firstVisibleCommand } from './commands';
|
||||
import type { ComponentType } from 'svelte';
|
||||
import { default as RewindCommand } from './RewindCommand.svelte';
|
||||
import { default as HelpCommand } from './HelpCommand.svelte';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
|
||||
const matchFiles = (params: { projectId: string; matchPattern: string }) =>
|
||||
invoke<Array<string>>('git_match_paths', params);
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import tinykeys, { type KeyBindingMap } from 'tinykeys';
|
||||
import CmdK from './CmdK.svelte';
|
||||
import Commit from './Commit.svelte';
|
||||
import Replay from './Replay.svelte';
|
||||
import Branch from './Branch.svelte';
|
||||
|
||||
const debounce = <T extends (...args: any[]) => any>(fn: T, delay: number) => {
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
return (...args: any[]) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => fn(...args), delay);
|
||||
};
|
||||
};
|
||||
let dialog: ComponentType | undefined;
|
||||
|
||||
let matchFilesQuery = '';
|
||||
const updateMatchFilesQuery = debounce(async () => {
|
||||
matchFilesQuery = userInput;
|
||||
}, 100);
|
||||
function isEventTargetInputOrTextArea(target: any) {
|
||||
if (target === null) return false;
|
||||
|
||||
let matchingFiles: Array<string> = [];
|
||||
$: if (matchFilesQuery) {
|
||||
matchFiles({ projectId: $currentProject?.id || '', matchPattern: matchFilesQuery }).then(
|
||||
(files) => {
|
||||
matchingFiles = files;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
matchingFiles = [];
|
||||
const targetElementName = target.tagName.toLowerCase();
|
||||
return ['input', 'textarea'].includes(targetElementName);
|
||||
}
|
||||
|
||||
$: scopeToProject = $currentProject ? true : false;
|
||||
|
||||
let showingCommandPalette = false;
|
||||
let dialog: HTMLDialogElement;
|
||||
let userInput: string;
|
||||
|
||||
let projects: Readable<any> = getContext('projects');
|
||||
|
||||
let selection: [number, number] = [0, 0];
|
||||
// if the group or the command are no longer visible, select the first visible group and first visible command
|
||||
$: if (
|
||||
!commandGroups[selection[0]]?.visible ||
|
||||
!commandGroups[selection[0]].commands[selection[1]]?.visible
|
||||
) {
|
||||
selection = firstVisibleCommand(commandGroups);
|
||||
function hotkeys(target: Window | HTMLElement, bindings: KeyBindingMap, disableOnInputs = true) {
|
||||
const wrappedBindings = disableOnInputs
|
||||
? Object.fromEntries(
|
||||
Object.entries(bindings).map(([key, handler]) => [
|
||||
key,
|
||||
(event: KeyboardEvent) => {
|
||||
if (!isEventTargetInputOrTextArea(event.target)) {
|
||||
handler(event);
|
||||
}
|
||||
}
|
||||
])
|
||||
)
|
||||
: bindings;
|
||||
return tinykeys(target, wrappedBindings);
|
||||
}
|
||||
$: selectedCommand = commandGroups[selection[0]].commands[selection[1]];
|
||||
$: {
|
||||
const element = document.getElementById(`${selection[0]}-${selection[1]}`);
|
||||
if (element) {
|
||||
// TODO: this works, but it's not standard
|
||||
element.scrollIntoViewIfNeeded(false);
|
||||
}
|
||||
}
|
||||
|
||||
let componentOfTriggeredCommand: ComponentType | undefined;
|
||||
let triggeredCommand: Command | undefined;
|
||||
|
||||
$: commandGroups = [
|
||||
{
|
||||
name: 'Go to project',
|
||||
visible: !scopeToProject,
|
||||
commands: $projects.map((project: Project) => {
|
||||
return {
|
||||
title: project.title,
|
||||
description: 'Repository',
|
||||
selected: false,
|
||||
action: {
|
||||
href: `/projects/${project.id}/`
|
||||
},
|
||||
visible: project.title.toLowerCase().includes(userInput?.toLowerCase())
|
||||
};
|
||||
})
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
visible: scopeToProject,
|
||||
commands: [
|
||||
{
|
||||
title: 'Replay History',
|
||||
description: 'Command',
|
||||
selected: false,
|
||||
action: {
|
||||
component: RewindCommand
|
||||
},
|
||||
visible: 'replay'.includes(userInput?.toLowerCase())
|
||||
},
|
||||
{
|
||||
title: 'Help',
|
||||
description: 'Command',
|
||||
selected: false,
|
||||
action: {
|
||||
component: HelpCommand
|
||||
},
|
||||
visible: 'help'.includes(userInput?.toLowerCase())
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Files',
|
||||
visible: scopeToProject,
|
||||
description: !userInput
|
||||
? 'type part of a file name'
|
||||
: matchingFiles.length === 0
|
||||
? `no files containing '${userInput}'`
|
||||
: '',
|
||||
commands: matchingFiles.map((file) => {
|
||||
return {
|
||||
title: file,
|
||||
description: 'File',
|
||||
selected: false,
|
||||
action: {
|
||||
href: `/`
|
||||
},
|
||||
visible: true
|
||||
};
|
||||
})
|
||||
}
|
||||
] as CommandGroup[];
|
||||
|
||||
const resetState = () => {
|
||||
userInput = '';
|
||||
scopeToProject = $currentProject ? true : false;
|
||||
selection = [0, 0];
|
||||
componentOfTriggeredCommand = undefined;
|
||||
triggeredCommand = undefined;
|
||||
matchingFiles = [];
|
||||
};
|
||||
|
||||
const triggerCommand = () => {
|
||||
if (
|
||||
!commandGroups[selection[0]].visible ||
|
||||
!commandGroups[selection[0]].commands[selection[1]].visible
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (Action.isLink(selectedCommand.action)) {
|
||||
toggleCommandPalette();
|
||||
goto(selectedCommand.action.href);
|
||||
} else if (Action.isActionInPalette(selectedCommand.action)) {
|
||||
userInput = '';
|
||||
componentOfTriggeredCommand = selectedCommand.action.component;
|
||||
triggeredCommand = selectedCommand;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleCommandPalette = () => {
|
||||
if (dialog && dialog.open) {
|
||||
dialog.close();
|
||||
showingCommandPalette = false;
|
||||
} else {
|
||||
resetState();
|
||||
dialog.showModal();
|
||||
showingCommandPalette = true;
|
||||
}
|
||||
};
|
||||
|
||||
let unsubscribeKeyboardHandler: () => void;
|
||||
|
||||
let unsubscribeKeyboardHandlerDisabledOnInput: () => void;
|
||||
onMount(() => {
|
||||
toggleCommandPalette(); // developmnet only
|
||||
unsubscribeKeyboardHandler = tinykeys(window, {
|
||||
'Meta+k': () => {
|
||||
toggleCommandPalette();
|
||||
},
|
||||
Backspace: () => {
|
||||
if (!userInput) {
|
||||
if (triggeredCommand) {
|
||||
// Untrigger command
|
||||
componentOfTriggeredCommand = undefined;
|
||||
triggeredCommand = undefined;
|
||||
} else {
|
||||
scopeToProject = false;
|
||||
}
|
||||
unsubscribeKeyboardHandler = hotkeys(
|
||||
window,
|
||||
{
|
||||
'Meta+k': () => {
|
||||
dialog === CmdK ? (dialog = undefined) : (dialog = CmdK);
|
||||
}
|
||||
},
|
||||
Enter: () => {
|
||||
triggerCommand();
|
||||
false // works even when an input is focused
|
||||
);
|
||||
unsubscribeKeyboardHandlerDisabledOnInput = hotkeys(
|
||||
window,
|
||||
{
|
||||
c: () => {
|
||||
dialog === Commit ? (dialog = undefined) : (dialog = Commit);
|
||||
},
|
||||
r: () => {
|
||||
dialog === Replay ? (dialog = undefined) : (dialog = Replay);
|
||||
},
|
||||
b: () => {
|
||||
dialog === Branch ? (dialog = undefined) : (dialog = Branch);
|
||||
}
|
||||
},
|
||||
ArrowDown: () => {
|
||||
selection = nextCommand(commandGroups, selection);
|
||||
},
|
||||
ArrowUp: () => {
|
||||
selection = previousCommand(commandGroups, selection);
|
||||
},
|
||||
'Control+n': () => {
|
||||
selection = nextCommand(commandGroups, selection);
|
||||
},
|
||||
'Control+p': () => {
|
||||
selection = previousCommand(commandGroups, selection);
|
||||
}
|
||||
});
|
||||
true // disabled when an input is focused
|
||||
);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribeKeyboardHandler?.();
|
||||
unsubscribeKeyboardHandlerDisabledOnInput?.();
|
||||
});
|
||||
|
||||
const onDialogClose = () => {
|
||||
dialog = undefined;
|
||||
};
|
||||
|
||||
const onNewDialog = (e: CustomEvent) => {
|
||||
dialog = e.detail;
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<dialog
|
||||
class="rounded-lg
|
||||
border border-zinc-400/40
|
||||
bg-zinc-900/70 p-0 backdrop-blur-xl
|
||||
"
|
||||
bind:this={dialog}
|
||||
on:click|self={() => toggleCommandPalette()}
|
||||
>
|
||||
<div class="flex h-[640px] w-[640px] flex-col rounded text-zinc-400" on:click|stopPropagation>
|
||||
<!-- Search input area -->
|
||||
<div class="flex items-center border-b border-zinc-400/20 py-2">
|
||||
<div class="ml-4 mr-2 flex flex-grow items-center">
|
||||
<!-- Project scope -->
|
||||
{#if scopeToProject}
|
||||
<div class="mr-1 flex items-center">
|
||||
<span class="font-semibold text-zinc-300">{$currentProject?.title}</span>
|
||||
<span class="ml-1 text-lg">/</span>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Selected command -->
|
||||
{#if scopeToProject && triggeredCommand}
|
||||
<div class="mr-1 flex items-center">
|
||||
<span class="font-semibold text-zinc-300">{triggeredCommand?.title}</span>
|
||||
<span class="ml-1 text-lg">/</span>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Search input -->
|
||||
<div class="mr-1 flex-grow">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
class="w-full bg-transparent text-zinc-300 focus:outline-none"
|
||||
bind:value={userInput}
|
||||
on:input={updateMatchFilesQuery}
|
||||
type="text"
|
||||
autofocus
|
||||
placeholder={!scopeToProject
|
||||
? 'Search for repositories'
|
||||
: !componentOfTriggeredCommand
|
||||
? 'Search for commands, files and code changes...'
|
||||
: ''}
|
||||
/>
|
||||
</div>
|
||||
<button on:click={toggleCommandPalette} class="rounded p-2 hover:bg-zinc-600">
|
||||
<IconCircleCancel class="fill-zinc-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Main part -->
|
||||
<div class="flex-auto overflow-y-auto">
|
||||
{#if componentOfTriggeredCommand}
|
||||
<svelte:component this={componentOfTriggeredCommand} {userInput} />
|
||||
{:else}
|
||||
{#each commandGroups as group, groupIdx}
|
||||
{#if group.visible}
|
||||
<div class="mx-2 cursor-default select-none">
|
||||
<p
|
||||
class="mx-2 cursor-default select-none py-2 text-sm font-semibold text-zinc-300/80"
|
||||
>
|
||||
<span>{group.name}</span>
|
||||
{#if group.description}
|
||||
<span class="ml-2 font-light italic text-zinc-300/60">({group.description})</span>
|
||||
{/if}
|
||||
</p>
|
||||
<ul class="">
|
||||
{#each group.commands as command, commandIdx}
|
||||
{#if command.visible}
|
||||
{#if Action.isLink(command.action)}
|
||||
<a
|
||||
on:mouseover={() => (selection = [groupIdx, commandIdx])}
|
||||
on:focus={() => (selection = [groupIdx, commandIdx])}
|
||||
id={`${groupIdx}-${commandIdx}`}
|
||||
href={command.action.href}
|
||||
class="{selection[0] === groupIdx && selection[1] === commandIdx
|
||||
? 'bg-zinc-700/70'
|
||||
: ''} flex cursor-default items-center rounded-lg p-2 px-2 outline-none"
|
||||
>
|
||||
<span class="flex-grow">{command.title}</span>
|
||||
<span>{command.description}</span>
|
||||
</a>
|
||||
{:else if Action.isActionInPalette(command.action)}
|
||||
<div
|
||||
on:mouseover={() => (selection = [groupIdx, commandIdx])}
|
||||
on:focus={() => (selection = [groupIdx, commandIdx])}
|
||||
on:click={triggerCommand}
|
||||
class="{selection[0] === groupIdx && selection[1] === commandIdx
|
||||
? 'bg-zinc-700/70'
|
||||
: ''} flex cursor-default items-center rounded-lg p-2 px-2 outline-none"
|
||||
>
|
||||
<span class="flex-grow">{command.title}</span>
|
||||
<span>{command.description}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
{#if dialog}
|
||||
<svelte:component this={dialog} on:close={onDialogClose} on:newdialog={onNewDialog} />
|
||||
{/if}
|
||||
|
114
src/lib/components/CommandPalette/Commit.svelte
Normal file
114
src/lib/components/CommandPalette/Commit.svelte
Normal file
@ -0,0 +1,114 @@
|
||||
<script lang="ts">
|
||||
import BaseDialog from './BaseDialog.svelte';
|
||||
import { shortPath } from '$lib/paths';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { currentProject } from '$lib/current_project';
|
||||
import { onMount } from 'svelte';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let commitMessage = '';
|
||||
|
||||
let changedFiles: Record<string, string> = {};
|
||||
|
||||
const listFiles = (params: { projectId: string }) =>
|
||||
invoke<Record<string, string>>('git_status', params);
|
||||
|
||||
const commit = (params: {
|
||||
projectId: string;
|
||||
message: string;
|
||||
files: Array<string>;
|
||||
push: boolean;
|
||||
}) => invoke<boolean>('git_commit', params);
|
||||
|
||||
onMount(() => {
|
||||
listFiles({ projectId: $currentProject?.id || '' }).then((files) => {
|
||||
changedFiles = files;
|
||||
});
|
||||
});
|
||||
|
||||
function doCommit() {
|
||||
// get checked files
|
||||
let changedFiles: Array<string> = [];
|
||||
let doc = document.getElementsByClassName('file-checkbox');
|
||||
Array.from(doc).forEach((c: any) => {
|
||||
if (c.checked) {
|
||||
changedFiles.push(c.dataset['file']);
|
||||
}
|
||||
});
|
||||
if ($currentProject) {
|
||||
commit({
|
||||
projectId: $currentProject.id,
|
||||
message: commitMessage,
|
||||
files: changedFiles,
|
||||
push: false
|
||||
}).then((result) => {
|
||||
toast.success('Commit successful!', {
|
||||
icon: '🎉'
|
||||
});
|
||||
commitMessage = '';
|
||||
dispatch('close');
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<BaseDialog on:close>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="flex flex-col rounded text-zinc-400" on:click|stopPropagation>
|
||||
<div class="mb-4 w-full border-b border-zinc-700 p-4 text-lg text-white">
|
||||
Commit Your Changes
|
||||
</div>
|
||||
<div
|
||||
class="relative mx-auto transform overflow-hidden p-2 text-left transition-all sm:w-full sm:max-w-sm"
|
||||
>
|
||||
{#if Object.entries(changedFiles).length > 0}
|
||||
<div>
|
||||
<div class="">
|
||||
<h3 class="text-base font-semibold text-zinc-200" id="modal-title">Commit Message</h3>
|
||||
<div class="mt-2">
|
||||
<div class="mt-2">
|
||||
<textarea
|
||||
rows="4"
|
||||
name="message"
|
||||
placeholder="Description of changes"
|
||||
id="commit-message"
|
||||
bind:value={commitMessage}
|
||||
class="block w-full rounded-md border-0 p-4 text-zinc-200 ring-1 ring-inset ring-gray-600 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:py-1.5 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 sm:mt-6">
|
||||
<button
|
||||
type="button"
|
||||
on:click={doCommit}
|
||||
class="inline-flex w-full justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>Commit Your Changes</button
|
||||
>
|
||||
</div>
|
||||
<div class="mt-4 py-4 text-zinc-200">
|
||||
<h3 class="text-base font-semibold text-zinc-200" id="modal-title">Changed Files</h3>
|
||||
{#each Object.entries(changedFiles) as file}
|
||||
<div class="flex flex-row space-x-2">
|
||||
<div>
|
||||
<input type="checkbox" class="file-checkbox" data-file={file[0]} checked />
|
||||
</div>
|
||||
<div>
|
||||
{file[1]}
|
||||
</div>
|
||||
<div class="font-mono">
|
||||
{shortPath(file[0])}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mx-auto text-center text-white">No changes to commit</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
@ -1,108 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Action, firstVisibleSubCommand, nextSubCommand, previousSubCommand } from './commands';
|
||||
import tinykeys from 'tinykeys';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { open } from '@tauri-apps/api/shell';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let selection = 0;
|
||||
|
||||
$: selectedCommand = innerCommands[selection];
|
||||
|
||||
const triggerCommand = () => {
|
||||
if (!innerCommands[selection].visible) {
|
||||
return;
|
||||
}
|
||||
if (Action.isLink(selectedCommand.action)) {
|
||||
console.log('triggerCommand');
|
||||
// toggleCommandPalette();
|
||||
// goto(selectedCommand.action.href);
|
||||
open(selectedCommand.action.href);
|
||||
}
|
||||
};
|
||||
|
||||
let unsubscribeKeyboardHandler: () => void;
|
||||
|
||||
onMount(() => {
|
||||
unsubscribeKeyboardHandler = tinykeys(window, {
|
||||
ArrowDown: () => {
|
||||
selection = nextSubCommand(innerCommands, selection);
|
||||
},
|
||||
ArrowUp: () => {
|
||||
selection = previousSubCommand(innerCommands, selection);
|
||||
},
|
||||
'Control+n': () => {
|
||||
selection = nextSubCommand(innerCommands, selection);
|
||||
},
|
||||
'Control+p': () => {
|
||||
selection = previousSubCommand(innerCommands, selection);
|
||||
},
|
||||
Enter: () => {
|
||||
triggerCommand();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribeKeyboardHandler?.();
|
||||
});
|
||||
|
||||
export let userInput: string;
|
||||
|
||||
$: innerCommands = [
|
||||
{
|
||||
title: 'Open Documentation',
|
||||
description: 'External link',
|
||||
selected: false,
|
||||
action: {
|
||||
href: 'https://docs.gitbutler.com'
|
||||
},
|
||||
visible: 'documentation'.includes(userInput?.toLowerCase())
|
||||
},
|
||||
{
|
||||
title: 'Join Discord Server',
|
||||
description: 'External link',
|
||||
selected: false,
|
||||
action: {
|
||||
href: 'https://discord.gg/MmFkmaJ42D'
|
||||
},
|
||||
visible: 'discord server'.includes(userInput?.toLowerCase())
|
||||
},
|
||||
{
|
||||
title: 'Email Support',
|
||||
description: 'External link',
|
||||
selected: false,
|
||||
action: {
|
||||
href: 'mailto:hello@gitbutler.com'
|
||||
},
|
||||
visible: 'discord server'.includes(userInput?.toLowerCase())
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="mx-2 cursor-default select-none">
|
||||
<p class="mx-2 cursor-default select-none py-2 text-sm font-semibold text-zinc-300/80">Help</p>
|
||||
|
||||
<ul class="">
|
||||
{#each innerCommands as command, commandIdx}
|
||||
{#if command.visible}
|
||||
{#if Action.isLink(command.action)}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
on:mouseover={() => (selection = commandIdx)}
|
||||
on:focus={() => (selection = commandIdx)}
|
||||
on:click={triggerCommand}
|
||||
href={command.action.href}
|
||||
class="{selection === commandIdx
|
||||
? 'bg-zinc-700/70'
|
||||
: ''} flex cursor-default items-center rounded-lg p-2 px-2 outline-none"
|
||||
>
|
||||
<span class="flex-grow">{command.title}</span>
|
||||
<span>{command.description}</span>
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
7
src/lib/components/CommandPalette/Replay.svelte
Normal file
7
src/lib/components/CommandPalette/Replay.svelte
Normal file
@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import BaseDialog from './BaseDialog.svelte';
|
||||
</script>
|
||||
|
||||
<BaseDialog on:close>
|
||||
<h1>Replay Working History</h1>
|
||||
</BaseDialog>
|
@ -1,111 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Command } from './commands';
|
||||
import { Action, firstVisibleSubCommand, nextSubCommand, previousSubCommand } from './commands';
|
||||
import tinykeys from 'tinykeys';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
let unsubscribeKeyboardHandler: () => void;
|
||||
|
||||
onMount(() => {
|
||||
unsubscribeKeyboardHandler = tinykeys(window, {
|
||||
ArrowDown: () => {
|
||||
selection = nextSubCommand(innerCommands, selection);
|
||||
},
|
||||
ArrowUp: () => {
|
||||
selection = previousSubCommand(innerCommands, selection);
|
||||
},
|
||||
'Control+n': () => {
|
||||
selection = nextSubCommand(innerCommands, selection);
|
||||
},
|
||||
'Control+p': () => {
|
||||
selection = previousSubCommand(innerCommands, selection);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribeKeyboardHandler?.();
|
||||
});
|
||||
|
||||
export let userInput: string;
|
||||
|
||||
$: innerCommands = [
|
||||
{
|
||||
title: 'Last 1 hour',
|
||||
description: 'Command',
|
||||
selected: false,
|
||||
action: {
|
||||
href: '/'
|
||||
},
|
||||
visible: 'last 1 hour'.includes(userInput?.toLowerCase())
|
||||
},
|
||||
{
|
||||
title: 'Last 3 hours',
|
||||
description: 'Command',
|
||||
selected: false,
|
||||
action: {
|
||||
href: '/'
|
||||
},
|
||||
visible: 'last 3 hours'.includes(userInput?.toLowerCase())
|
||||
},
|
||||
{
|
||||
title: 'Last 6 hours',
|
||||
description: 'Command',
|
||||
selected: false,
|
||||
action: {
|
||||
href: '/'
|
||||
},
|
||||
visible: 'last 6 hours'.includes(userInput?.toLowerCase())
|
||||
},
|
||||
{
|
||||
title: 'Yesterday morning',
|
||||
description: 'Command',
|
||||
selected: false,
|
||||
action: {
|
||||
href: '/'
|
||||
},
|
||||
visible: 'yesterday morning'.includes(userInput?.toLowerCase())
|
||||
},
|
||||
{
|
||||
title: 'Yesterday afternoon',
|
||||
description: 'Command',
|
||||
selected: false,
|
||||
action: {
|
||||
href: '/'
|
||||
},
|
||||
visible: 'yesterday afternoon'.includes(userInput?.toLowerCase())
|
||||
}
|
||||
] as Command[];
|
||||
|
||||
let selection = 0;
|
||||
|
||||
$: if (!innerCommands[selection]?.visible) {
|
||||
selection = firstVisibleSubCommand(innerCommands);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-2 cursor-default select-none">
|
||||
<p class="mx-2 cursor-default select-none py-2 text-sm font-semibold text-zinc-300/80">
|
||||
Replay...
|
||||
</p>
|
||||
|
||||
<ul class="">
|
||||
{#each innerCommands as command, commandIdx}
|
||||
{#if command.visible}
|
||||
{#if Action.isLink(command.action)}
|
||||
<a
|
||||
on:mouseover={() => (selection = commandIdx)}
|
||||
on:focus={() => (selection = commandIdx)}
|
||||
href={command.action.href}
|
||||
class="{selection === commandIdx
|
||||
? 'bg-zinc-700/70'
|
||||
: ''} flex cursor-default items-center rounded-lg p-2 px-2 outline-none"
|
||||
>
|
||||
<span class="flex-grow">{command.title}</span>
|
||||
<span>{command.description}</span>
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
@ -1,3 +1,3 @@
|
||||
import { default as CommandPaletteNext } from './CommandPalette.svelte';
|
||||
import { default as CommandPalette } from './CommandPalette.svelte';
|
||||
|
||||
export default CommandPaletteNext;
|
||||
export default CommandPalette;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import fi from 'date-fns/esm/locale/fi/index.js';
|
||||
import type { ComponentType } from 'svelte';
|
||||
|
||||
export type ActionLink = { href: string };
|
@ -2,3 +2,4 @@ export { default as BackForwardButtons } from './BackForwardButtons.svelte';
|
||||
export { default as Login } from './Login.svelte';
|
||||
export { default as Breadcrumbs } from './Breadcrumbs.svelte';
|
||||
export { default as CodeViewer } from './CodeViewer';
|
||||
export { default as CommandPalette } from './CommandPalette';
|
||||
|
@ -7,7 +7,7 @@
|
||||
import { setContext } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import Breadcrumbs from '$lib/components/Breadcrumbs.svelte';
|
||||
import CommandPalette from '$lib/components/CommandPalette.svelte';
|
||||
import CommandPalette from '$lib/components/CommandPalette';
|
||||
|
||||
export let data: LayoutData;
|
||||
const { user, posthog, projects } = data;
|
||||
@ -45,5 +45,5 @@
|
||||
<slot />
|
||||
</div>
|
||||
<Toaster />
|
||||
<CommandPalette {projects} />
|
||||
<CommandPalette />
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user