mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-26 11:08:38 +03:00
Merge branch 'master' of github.com:gitbutlerapp/gitbutler-client
This commit is contained in:
commit
d6186a06fc
@ -7,8 +7,10 @@
|
||||
import { IconCircleCancel } from '$lib/components/icons';
|
||||
import type { Project } from '$lib/projects';
|
||||
import tinykeys from 'tinykeys';
|
||||
import type { CommandGroup } from './commands';
|
||||
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';
|
||||
|
||||
$: scopeToProject = $currentProject ? true : false;
|
||||
|
||||
@ -26,6 +28,10 @@
|
||||
) {
|
||||
selection = firstVisibleCommand(commandGroups);
|
||||
}
|
||||
$: selectedCommand = commandGroups[selection[0]].commands[selection[1]];
|
||||
|
||||
let componentOfTriggeredCommand: ComponentType | undefined;
|
||||
let triggeredCommand: Command | undefined;
|
||||
|
||||
$: commandGroups = [
|
||||
{
|
||||
@ -42,6 +48,21 @@
|
||||
visible: project.title.toLowerCase().includes(userInput?.toLowerCase())
|
||||
};
|
||||
})
|
||||
},
|
||||
{
|
||||
name: 'Commands',
|
||||
visible: scopeToProject,
|
||||
commands: [
|
||||
{
|
||||
title: 'Replay',
|
||||
description: 'Command',
|
||||
selected: false,
|
||||
action: {
|
||||
component: RewindCommand
|
||||
},
|
||||
visible: 'replay'.includes(userInput?.toLowerCase())
|
||||
}
|
||||
]
|
||||
}
|
||||
] as CommandGroup[];
|
||||
|
||||
@ -49,16 +70,24 @@
|
||||
userInput = '';
|
||||
scopeToProject = $currentProject ? true : false;
|
||||
selection = [0, 0];
|
||||
componentOfTriggeredCommand = undefined;
|
||||
triggeredCommand = undefined;
|
||||
};
|
||||
|
||||
const handleEnter = () => {
|
||||
if (!commandGroups[0].visible || !commandGroups[0].commands[0].visible) {
|
||||
const triggerCommand = () => {
|
||||
if (
|
||||
!commandGroups[selection[0]].visible ||
|
||||
!commandGroups[selection[0]].commands[selection[1]].visible
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const command = commandGroups[selection[0]].commands[selection[1]];
|
||||
if (Action.isLink(command.action)) {
|
||||
if (Action.isLink(selectedCommand.action)) {
|
||||
toggleCommandPalette();
|
||||
goto(command.action.href);
|
||||
goto(selectedCommand.action.href);
|
||||
} else if (Action.isActionInPalette(selectedCommand.action)) {
|
||||
userInput = '';
|
||||
componentOfTriggeredCommand = selectedCommand.action.component;
|
||||
triggeredCommand = selectedCommand;
|
||||
}
|
||||
};
|
||||
|
||||
@ -76,18 +105,24 @@
|
||||
let unsubscribeKeyboardHandler: () => void;
|
||||
|
||||
onMount(() => {
|
||||
// toggleCommandPalette(); // developmnet only
|
||||
toggleCommandPalette(); // developmnet only
|
||||
unsubscribeKeyboardHandler = tinykeys(window, {
|
||||
'Meta+k': () => {
|
||||
toggleCommandPalette();
|
||||
},
|
||||
Backspace: () => {
|
||||
if (!userInput) {
|
||||
scopeToProject = false;
|
||||
if (triggeredCommand) {
|
||||
// Untrigger command
|
||||
componentOfTriggeredCommand = undefined;
|
||||
triggeredCommand = undefined;
|
||||
} else {
|
||||
scopeToProject = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
Enter: () => {
|
||||
handleEnter();
|
||||
triggerCommand();
|
||||
},
|
||||
ArrowDown: () => {
|
||||
selection = nextCommand(commandGroups, selection);
|
||||
@ -129,6 +164,13 @@
|
||||
<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 -->
|
||||
@ -137,9 +179,11 @@
|
||||
bind:value={userInput}
|
||||
type="text"
|
||||
autofocus
|
||||
placeholder={scopeToProject
|
||||
placeholder={!scopeToProject
|
||||
? 'Search for repositories'
|
||||
: !componentOfTriggeredCommand
|
||||
? 'Search for commands, files and code changes...'
|
||||
: 'Search for repositories'}
|
||||
: ''}
|
||||
/>
|
||||
</div>
|
||||
<button on:click={toggleCommandPalette} class="rounded p-2 hover:bg-zinc-600">
|
||||
@ -149,34 +193,54 @@
|
||||
</div>
|
||||
<!-- Main part -->
|
||||
<div>
|
||||
{#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">
|
||||
{group.name}
|
||||
</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])}
|
||||
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>
|
||||
{#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"
|
||||
>
|
||||
{group.name}
|
||||
</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])}
|
||||
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}
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
{scopeToProject}
|
||||
{selection}
|
||||
|
141
src/lib/components/CommandPalette/RewindCommand.svelte
Normal file
141
src/lib/components/CommandPalette/RewindCommand.svelte
Normal file
@ -0,0 +1,141 @@
|
||||
<script lang="ts">
|
||||
import type { Command } from './commands';
|
||||
import { Action, previousCommand, nextCommand, firstVisibleCommand } 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: '/foo'
|
||||
},
|
||||
visible: 'last 1 hour'.includes(userInput?.toLowerCase())
|
||||
},
|
||||
{
|
||||
title: 'Last 3 hours',
|
||||
description: 'Command',
|
||||
selected: false,
|
||||
action: {
|
||||
href: '/foo'
|
||||
},
|
||||
visible: 'last 3 hours'.includes(userInput?.toLowerCase())
|
||||
},
|
||||
{
|
||||
title: 'Last 6 hours',
|
||||
description: 'Command',
|
||||
selected: false,
|
||||
action: {
|
||||
href: '/foo'
|
||||
},
|
||||
visible: 'last 6 hours'.includes(userInput?.toLowerCase())
|
||||
},
|
||||
{
|
||||
title: 'Yesterday morning',
|
||||
description: 'Command',
|
||||
selected: false,
|
||||
action: {
|
||||
href: '/foo'
|
||||
},
|
||||
visible: 'yesterday morning'.includes(userInput?.toLowerCase())
|
||||
},
|
||||
{
|
||||
title: 'Yesterday afternoon',
|
||||
description: 'Command',
|
||||
selected: false,
|
||||
action: {
|
||||
href: '/foo'
|
||||
},
|
||||
visible: 'yesterday afternoon'.includes(userInput?.toLowerCase())
|
||||
}
|
||||
] as Command[];
|
||||
|
||||
let selection = 0;
|
||||
|
||||
$: if (!innerCommands[selection]?.visible) {
|
||||
selection = firstVisibleSubCommand(innerCommands);
|
||||
}
|
||||
|
||||
const firstVisibleSubCommand = (commands: Command[]): number => {
|
||||
const firstVisibleGroup = commands.findIndex((command) => command.visible);
|
||||
if (firstVisibleGroup === -1) {
|
||||
return 0;
|
||||
}
|
||||
return firstVisibleGroup;
|
||||
};
|
||||
|
||||
const nextSubCommand = (commands: Command[], selection: number): number => {
|
||||
const nextVisibleCommandIndex = commands
|
||||
.slice(selection + 1)
|
||||
.findIndex((command) => command.visible);
|
||||
|
||||
if (nextVisibleCommandIndex !== -1) {
|
||||
return selection + 1 + nextVisibleCommandIndex;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const previousSubCommand = (commands: Command[], selection: number): number => {
|
||||
const previousVisibleCommandIndex = commands
|
||||
.slice(0, selection)
|
||||
.reverse()
|
||||
.findIndex((command) => command.visible);
|
||||
if (previousVisibleCommandIndex !== -1) {
|
||||
return selection - 1 - previousVisibleCommandIndex;
|
||||
}
|
||||
return commands.length - 1;
|
||||
};
|
||||
</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,16 +1,18 @@
|
||||
import type { ComponentType } from 'svelte';
|
||||
|
||||
export type ActionLink = { href: string };
|
||||
export type ActionInPalette = { action: () => void }; // todo
|
||||
export type ActionInPalette = { component: ComponentType };
|
||||
export type Action = ActionLink | ActionInPalette;
|
||||
|
||||
export namespace Action {
|
||||
export const isLink = (action: Action): action is ActionLink => 'href' in action;
|
||||
export const isActionInPalette = (action: Action): action is ActionInPalette => 'todo' in action;
|
||||
export const isActionInPalette = (action: Action): action is ActionInPalette =>
|
||||
'component' in action;
|
||||
}
|
||||
|
||||
export type Command = {
|
||||
title: string;
|
||||
description: string;
|
||||
// icon: string;
|
||||
action: Action;
|
||||
selected: boolean;
|
||||
visible: boolean;
|
||||
|
Loading…
Reference in New Issue
Block a user