From b31441b8882b2c300dac48534e2854d7f1ffa538 Mon Sep 17 00:00:00 2001 From: Nikita Galaiko Date: Mon, 6 Nov 2023 11:41:18 +0100 Subject: [PATCH 01/11] drag & amend works --- packages/ui/src/lib/dragable.ts | 178 ++++++++++++++++++ .../[projectId]/VirtualBranchPeek.svelte | 2 +- .../[projectId]/components/BranchLane.svelte | 103 +++++----- .../[projectId]/components/CommitCard.svelte | 97 +++++++--- .../[projectId]/components/FileCard.svelte | 27 ++- 5 files changed, 306 insertions(+), 101 deletions(-) create mode 100644 packages/ui/src/lib/dragable.ts diff --git a/packages/ui/src/lib/dragable.ts b/packages/ui/src/lib/dragable.ts new file mode 100644 index 000000000..1fcbd8963 --- /dev/null +++ b/packages/ui/src/lib/dragable.ts @@ -0,0 +1,178 @@ +// + +export interface Dropzone { + disabled: boolean; + active: string; + hover: string; + accepts: (data: any) => boolean; + onDrop: (data: any) => Promise | void; +} + +const defaultDropzoneOptions: Dropzone = { + disabled: false, + active: 'dropzone-active', + hover: 'dropzone-hover', + accepts: (data) => data === 'default', + onDrop: () => {} +}; + +export function dropzone(node: HTMLElement, opts: Partial | undefined) { + const options = { ...defaultDropzoneOptions, ...opts }; + + if (options.disabled) return; + + register(node, options); + + function handleDragEnter(e: DragEvent) { + if (activeZones.has(node)) { + node.classList.add(options.hover); + e.preventDefault(); + } + } + + function handleDragLeave(_e: DragEvent) { + if (activeZones.has(node)) { + node.classList.remove(options.hover); + } + } + + function handleDragOver(e: DragEvent) { + if (activeZones.has(node)) { + e.preventDefault(); + } + } + + node.addEventListener('dragenter', handleDragEnter); + node.addEventListener('dragleave', handleDragLeave); + node.addEventListener('dragover', handleDragOver); + + return { + destroy() { + unregister(options); + + node.removeEventListener('dragenter', handleDragEnter); + node.removeEventListener('dragleave', handleDragLeave); + node.removeEventListener('dragover', handleDragOver); + } + }; +} + +// + +const registry: [HTMLElement, Dropzone][] = []; + +const activeZones = new Set(); + +function register(node: HTMLElement, dropzone: Dropzone) { + registry.push([node, dropzone]); +} + +function unregister(dropzone: Dropzone) { + const index = registry.findIndex(([, dz]) => dz === dropzone); + if (index >= 0) registry.splice(index, 1); +} + +// + +export interface Dragable { + data: any; + disabled: boolean; +} + +const defaultDragableOptions: Dragable = { + data: {}, + disabled: false +}; + +export function dragable(node: HTMLElement, opts: Partial | undefined) { + const options = { ...defaultDragableOptions, ...opts }; + + if (options.disabled) return; + + node.draggable = true; + + let clone: HTMLElement; + + const onDropListeners = new Map void>>(); + + /** + * The problem with the ghost element is that it gets clipped after rotation unless we enclose + * it within a larger bounding box. This means we have an extra `
` in the html that is + * only present to support the rotation + */ + function handleDragStart(e: DragEvent) { + // Start by cloning the node for the ghost element + clone = node.cloneNode(true) as HTMLElement; + clone.style.position = 'absolute'; + clone.style.top = '-9999px'; // Element has to be in the DOM so we move it out of sight + clone.style.display = 'inline-block'; + clone.style.padding = '30px'; // To prevent clipping of rotated element + + // Style the inner node so it retains the shape and then rotate + const inner = clone.children[0] as HTMLElement; + inner.style.height = node.clientHeight + 'px'; + inner.style.width = node.clientWidth + 'px'; + inner.style.rotate = `${Math.floor(Math.random() * 3)}deg`; + document.body.appendChild(clone); + + // Dim the original element while dragging + node.style.opacity = '0.6'; + + // activate destination zones + registry + .filter(([_node, dz]) => dz.accepts(options.data)) + .forEach(([node, dz]) => { + const onDrop = (e: DragEvent) => { + e.preventDefault(); + dz.onDrop(options.data); + }; + + // keep track of listeners so that we can remove them later + if (onDropListeners.has(node)) { + onDropListeners.get(node)!.push(onDrop); + } else { + onDropListeners.set(node, [onDrop]); + } + + node.classList.add(dz.active); + node.addEventListener('drop', onDrop); + activeZones.add(node); + }); + + e.dataTransfer?.setDragImage(clone, e.offsetX + 30, e.offsetY + 30); // Adds the padding + e.stopPropagation(); + } + + function handleDragEnd(e: DragEvent) { + node.style.opacity = '1'; + clone.remove(); + + // deactivate destination zones + registry + .filter(([_node, dz]) => dz.accepts(options.data)) + .forEach(([node, dz]) => { + // remove all listeners + const onDrop = onDropListeners.get(node); + if (onDrop) { + onDrop.forEach((listener) => { + node.removeEventListener('drop', listener); + }); + } + + node.classList.remove(dz.active); + activeZones.delete(node); + }); + + e.stopPropagation(); + } + + node.addEventListener('dragstart', handleDragStart); + node.addEventListener('dragend', handleDragEnd); + + return { + destroy() { + node.removeEventListener('dragstart', handleDragStart); + node.removeEventListener('dragend', handleDragEnd); + } + }; +} diff --git a/packages/ui/src/routes/[projectId]/VirtualBranchPeek.svelte b/packages/ui/src/routes/[projectId]/VirtualBranchPeek.svelte index 853a7c519..72eee6104 100644 --- a/packages/ui/src/routes/[projectId]/VirtualBranchPeek.svelte +++ b/packages/ui/src/routes/[projectId]/VirtualBranchPeek.svelte @@ -88,7 +88,7 @@

Commits

{#each branch.commits as commit} - + {/each}
diff --git a/packages/ui/src/routes/[projectId]/components/BranchLane.svelte b/packages/ui/src/routes/[projectId]/components/BranchLane.svelte index 048749671..f8f2fb058 100644 --- a/packages/ui/src/routes/[projectId]/components/BranchLane.svelte +++ b/packages/ui/src/routes/[projectId]/components/BranchLane.svelte @@ -1,12 +1,12 @@ -
{ - if (!e.dataTransfer) { - return; - } - const data = e.dataTransfer.getData(dzType); - const [newFileId, newHunks] = data.split(':'); - const existingHunkIds = - branch.files.find((f) => f.id === newFileId)?.hunks.map((h) => h.id) || []; - const newHunkIds = newHunks.split(',').filter((h) => !existingHunkIds.includes(h)); - if (newHunkIds.length == 0) { - // don't allow dropping hunk to the lane where it already is - return; - } - e.stopPropagation(); - branchController.updateBranchOwnership(branch.id, (data + '\n' + branch.ownership).trim()); - }} -> +
{#each branch.upstream.commits as commit} -
e.dataTransfer?.setData('commit/upstream', commit.id)} - > - -
+ {/each}
{#if branchCount > 1} @@ -488,25 +477,11 @@ />
{ - if (!e.dataTransfer) { - return; - } - const targetCommitOid = e.dataTransfer.getData('commit/upstream'); - if (!targetCommitOid) { - return; - } - e.stopPropagation(); - branchController.cherryPick({ - targetCommitOid, - branchId: branch.id - }); + use:dropzone={{ + hover: 'lane-dz-hover', + active: 'lane-dz-active', + accepts: acceptBranchDrop, + onDrop: onBranchDrop }} > @@ -550,8 +525,8 @@ expanded={file.expanded} conflicted={file.conflicted} {selectedOwnership} + branchId={branch.id} {file} - {dzType} {projectId} {projectPath} {branchController} @@ -664,7 +639,12 @@
{/if} - +
{/each}
@@ -751,7 +731,12 @@ />
{/if} - +
{/each}
@@ -797,7 +782,7 @@ class:dark:bg-dark-500={commit.isRemote} /> - + {/each} diff --git a/packages/ui/src/routes/[projectId]/components/CommitCard.svelte b/packages/ui/src/routes/[projectId]/components/CommitCard.svelte index 7ade93f58..1e3a3ffac 100644 --- a/packages/ui/src/routes/[projectId]/components/CommitCard.svelte +++ b/packages/ui/src/routes/[projectId]/components/CommitCard.svelte @@ -1,6 +1,7 @@ -
-
-
- -
+
+ -
- Gravatar for {commit.author.email} -
{commit.author.name}
-
- +
+
+
+ +
+
+ +
+ Gravatar for {commit.author.email} +
{commit.author.name}
+
+ +
@@ -150,3 +185,13 @@
+ + diff --git a/packages/ui/src/routes/[projectId]/components/FileCard.svelte b/packages/ui/src/routes/[projectId]/components/FileCard.svelte index 55e03a617..46910cbe3 100644 --- a/packages/ui/src/routes/[projectId]/components/FileCard.svelte +++ b/packages/ui/src/routes/[projectId]/components/FileCard.svelte @@ -2,6 +2,7 @@ import { ContentSection, HunkSection, parseFileSections } from './fileSections'; import { createEventDispatcher, onDestroy } from 'svelte'; import type { File, Hunk } from '$lib/vbranches/types'; + import { dragable } from '$lib/dragable'; import type { Ownership } from '$lib/vbranches/ownership'; import type { Writable } from 'svelte/store'; import RenderedLine from './RenderedLine.svelte'; @@ -14,7 +15,6 @@ } from '$lib/icons'; import type { BranchController } from '$lib/vbranches/branchController'; import { getContext } from 'svelte'; - import { dzTrigger } from '$lib/utils/dropZone'; import IconExpandUpDownSlim from '$lib/icons/IconExpandUpDownSlim.svelte'; import { getVSIFileIcon } from '$lib/ext-icons'; import { slide } from 'svelte/transition'; @@ -24,10 +24,10 @@ import IconLock from '$lib/icons/IconLock.svelte'; import HunkContextMenu from './HunkContextMenu.svelte'; + export let branchId: string; export let file: File; export let conflicted: boolean; export let projectId: string; - export let dzType: string; export let projectPath: string | undefined; export let expanded: boolean | undefined; export let branchController: BranchController; @@ -77,10 +77,6 @@ $: minWidth = getGutterMinWidth(maxLineNumber); - function getAllHunksOwnership(): string { - return file.id + ':' + file.hunks.map((h) => h.id).join(','); - } - $: isFileLocked = sections .filter((section): section is HunkSection => section instanceof HunkSection) .some((section) => section.hunk.locked); @@ -100,10 +96,10 @@
e.dataTransfer?.setData('text/hunk', getAllHunksOwnership())} - role="group" + use:dragable={{ + data: { branchId, file }, + disabled: isFileLocked || readonly + }} class="changed-file inner" class:opacity-80={isFileLocked} > @@ -194,13 +190,14 @@ class="bg-6 border-color-3 my-1 flex w-full flex-col overflow-hidden rounded border" >
{ - if ('hunk' in section) - e.dataTransfer?.setData('text/hunk', file.id + ':' + section.hunk.id); + use:dragable={{ + data: { + branchId, + hunk: section.hunk + }, + disabled: section.hunk.locked || readonly }} on:dblclick class="changed-hunk" From 97cd2d4920fc31f5a3b4651bf9e4824c70eeb476 Mon Sep 17 00:00:00 2001 From: Nikita Galaiko Date: Mon, 6 Nov 2023 12:53:47 +0100 Subject: [PATCH 02/11] ammendable commit card --- .../ui/src/lib/vbranches/branchController.ts | 12 +++ .../components/AmendableCommitCard.svelte | 44 ++++++++++ .../[projectId]/components/BranchLane.svelte | 39 +++++---- .../[projectId]/components/CommitCard.svelte | 85 ++++++------------- 4 files changed, 106 insertions(+), 74 deletions(-) create mode 100644 packages/ui/src/routes/[projectId]/components/AmendableCommitCard.svelte diff --git a/packages/ui/src/lib/vbranches/branchController.ts b/packages/ui/src/lib/vbranches/branchController.ts index 163570022..0f59d3ef8 100644 --- a/packages/ui/src/lib/vbranches/branchController.ts +++ b/packages/ui/src/lib/vbranches/branchController.ts @@ -243,4 +243,16 @@ export class BranchController { toasts.error(`Failed to mark file resolved`); } } + + async amendBranch(params: { branchId: string; ownership: string }) { + try { + await invoke('amend_virtual_branch', { + projectId: this.projectId, + ...params + }); + await this.targetBranchStore.reload(); + } catch (err: any) { + toasts.error(`Failed to amend commit: ${err.message}`); + } + } } diff --git a/packages/ui/src/routes/[projectId]/components/AmendableCommitCard.svelte b/packages/ui/src/routes/[projectId]/components/AmendableCommitCard.svelte new file mode 100644 index 000000000..e4f7f4b3a --- /dev/null +++ b/packages/ui/src/routes/[projectId]/components/AmendableCommitCard.svelte @@ -0,0 +1,44 @@ + + +
+ + + +
diff --git a/packages/ui/src/routes/[projectId]/components/BranchLane.svelte b/packages/ui/src/routes/[projectId]/components/BranchLane.svelte index f8f2fb058..cc5a81d0b 100644 --- a/packages/ui/src/routes/[projectId]/components/BranchLane.svelte +++ b/packages/ui/src/routes/[projectId]/components/BranchLane.svelte @@ -39,6 +39,7 @@ import Button from '$lib/components/Button.svelte'; import Link from '$lib/components/Link.svelte'; import Modal from '$lib/components/Modal.svelte'; + import AmendableCommitCard from './AmendableCommitCard.svelte'; const [send, receive] = crossfade({ duration: (d) => Math.sqrt(d * 200), @@ -431,8 +432,8 @@ class="flex w-full flex-col border-t border-light-400 bg-light-300 p-2 dark:border-dark-400 dark:bg-dark-800" id="upstreamCommits" > - {#each branch.upstream.commits as commit} - + {#each branch.upstream.commits as commit (commit.id)} + {/each}
{#if branchCount > 1} @@ -639,12 +640,16 @@
{/if} - + {#if branch.commits.at(0)?.id === commit.id} + + {:else} + + {/if}
{/each}
@@ -731,12 +736,16 @@ />
{/if} - + {#if branch.commits.at(0)?.id === commit.id} + + {:else} + + {/if}
{/each}
@@ -782,7 +791,7 @@ class:dark:bg-dark-500={commit.isRemote} />
- + {/each} diff --git a/packages/ui/src/routes/[projectId]/components/CommitCard.svelte b/packages/ui/src/routes/[projectId]/components/CommitCard.svelte index 1e3a3ffac..246c09d84 100644 --- a/packages/ui/src/routes/[projectId]/components/CommitCard.svelte +++ b/packages/ui/src/routes/[projectId]/components/CommitCard.svelte @@ -1,7 +1,6 @@
-