mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-09-20 08:37:14 +03:00
Replace svelte-dnd-action with native drag & drop
This commit is a bit of a relief, we now have less than 1/10th the amount of code powering drag & drop. Future work includes: - extracting commonalities into a Svelte action - applying css to new drop zone without using sibling selector - styling
This commit is contained in:
parent
983d58061a
commit
2b19031a36
@ -82,7 +82,6 @@
|
|||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"svelte": "~3.55.1",
|
"svelte": "~3.55.1",
|
||||||
"svelte-check": "^3.0.1",
|
"svelte-check": "^3.0.1",
|
||||||
"svelte-dnd-action": "github:gitbutlerapp/svelte-dnd-action#9912712394c26b971ef067674bdc871550940636",
|
|
||||||
"svelte-floating-ui": "^1.5.2",
|
"svelte-floating-ui": "^1.5.2",
|
||||||
"svelte-french-toast": "^1.0.3",
|
"svelte-french-toast": "^1.0.3",
|
||||||
"svelte-loadable-store": "^1.2.3",
|
"svelte-loadable-store": "^1.2.3",
|
||||||
|
@ -193,9 +193,6 @@ devDependencies:
|
|||||||
svelte-check:
|
svelte-check:
|
||||||
specifier: ^3.0.1
|
specifier: ^3.0.1
|
||||||
version: 3.0.3(postcss-load-config@4.0.1)(postcss@8.4.21)(svelte@3.55.1)
|
version: 3.0.3(postcss-load-config@4.0.1)(postcss@8.4.21)(svelte@3.55.1)
|
||||||
svelte-dnd-action:
|
|
||||||
specifier: github:gitbutlerapp/svelte-dnd-action#9912712394c26b971ef067674bdc871550940636
|
|
||||||
version: github.com/gitbutlerapp/svelte-dnd-action/9912712394c26b971ef067674bdc871550940636(svelte@3.55.1)
|
|
||||||
svelte-floating-ui:
|
svelte-floating-ui:
|
||||||
specifier: ^1.5.2
|
specifier: ^1.5.2
|
||||||
version: 1.5.2
|
version: 1.5.2
|
||||||
@ -5462,17 +5459,6 @@ packages:
|
|||||||
engines: {node: '>=12.20'}
|
engines: {node: '>=12.20'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
github.com/gitbutlerapp/svelte-dnd-action/9912712394c26b971ef067674bdc871550940636(svelte@3.55.1):
|
|
||||||
resolution: {tarball: https://codeload.github.com/gitbutlerapp/svelte-dnd-action/tar.gz/9912712394c26b971ef067674bdc871550940636}
|
|
||||||
id: github.com/gitbutlerapp/svelte-dnd-action/9912712394c26b971ef067674bdc871550940636
|
|
||||||
name: svelte-dnd-action
|
|
||||||
version: 0.9.22
|
|
||||||
peerDependencies:
|
|
||||||
svelte: '>=3.23.0'
|
|
||||||
dependencies:
|
|
||||||
svelte: 3.55.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
github.com/tauri-apps/tauri-plugin-log/21921031d74f871180381317a338559f588ad8e9:
|
github.com/tauri-apps/tauri-plugin-log/21921031d74f871180381317a338559f588ad8e9:
|
||||||
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/21921031d74f871180381317a338559f588ad8e9}
|
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/21921031d74f871180381317a338559f588ad8e9}
|
||||||
name: tauri-plugin-log-api
|
name: tauri-plugin-log-api
|
||||||
|
@ -938,6 +938,7 @@ fn create_window(handle: &tauri::AppHandle) -> tauri::Result<tauri::Window> {
|
|||||||
tauri::WindowBuilder::new(handle, "main", tauri::WindowUrl::App("index.html".into()))
|
tauri::WindowBuilder::new(handle, "main", tauri::WindowUrl::App("index.html".into()))
|
||||||
.resizable(true)
|
.resizable(true)
|
||||||
.title(app_title)
|
.title(app_title)
|
||||||
|
.disable_file_drop_handler()
|
||||||
.min_inner_size(600.0, 300.0)
|
.min_inner_size(600.0, 300.0)
|
||||||
.inner_size(800.0, 600.0)
|
.inner_size(800.0, 600.0)
|
||||||
.build()
|
.build()
|
||||||
@ -952,6 +953,7 @@ fn create_window(handle: &tauri::AppHandle) -> tauri::Result<tauri::Window> {
|
|||||||
.min_inner_size(1024.0, 600.0)
|
.min_inner_size(1024.0, 600.0)
|
||||||
.inner_size(1024.0, 600.0)
|
.inner_size(1024.0, 600.0)
|
||||||
.hidden_title(true)
|
.hidden_title(true)
|
||||||
|
.disable_file_drop_handler()
|
||||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -316,3 +316,36 @@ input[type='checkbox'].large {
|
|||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* drag & drop */
|
||||||
|
|
||||||
|
.drag-zone-hover * {
|
||||||
|
@apply pointer-events-none;
|
||||||
|
}
|
||||||
|
.drag-zone-marker {
|
||||||
|
@apply border-green-450 bg-green-200 dark:bg-green-470;
|
||||||
|
}
|
||||||
|
.drag-zone-active.drag-zone-hover .drag-zone-marker,
|
||||||
|
.drag-zone-active + #new-branch-dz.drag-zone-hover .drag-zone-marker {
|
||||||
|
@apply bg-green-300 dark:bg-green-460;
|
||||||
|
}
|
||||||
|
.drag-zone-hover {
|
||||||
|
@apply border-green-500;
|
||||||
|
}
|
||||||
|
.drag-zone-active .no-changes {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
.drag-zone-active .drag-zone-marker {
|
||||||
|
@apply block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* drag & drop ugly stuff */
|
||||||
|
#new-branch-dz.new-branch-active {
|
||||||
|
@apply visible flex;
|
||||||
|
}
|
||||||
|
.drag-zone-active + #new-branch-dz .call-to-action {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
.drag-zone-active + #new-branch-dz .drag-zone-marker {
|
||||||
|
@apply block;
|
||||||
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
<script lang="ts" async="true">
|
<script lang="ts" async="true">
|
||||||
import { dndzone } from 'svelte-dnd-action';
|
|
||||||
import Lane from './BranchLane.svelte';
|
import Lane from './BranchLane.svelte';
|
||||||
import type { DndEvent } from 'svelte-dnd-action/typings';
|
|
||||||
import NewBranchDropZone from './NewBranchDropZone.svelte';
|
import NewBranchDropZone from './NewBranchDropZone.svelte';
|
||||||
import type { Branch } from '$lib/api/ipc/vbranches';
|
import type { Branch } from '$lib/api/ipc/vbranches';
|
||||||
import type { VirtualBranchOperations } from './vbranches';
|
import type { VirtualBranchOperations } from './vbranches';
|
||||||
@ -10,25 +8,12 @@
|
|||||||
export let projectPath: string;
|
export let projectPath: string;
|
||||||
export let branches: Branch[];
|
export let branches: Branch[];
|
||||||
export let virtualBranches: VirtualBranchOperations;
|
export let virtualBranches: VirtualBranchOperations;
|
||||||
|
let dragged: any;
|
||||||
|
let dropZone: HTMLDivElement;
|
||||||
|
let priorPosition = 0;
|
||||||
|
let dropPosition = 0;
|
||||||
|
|
||||||
const newBranchClass = 'new-branch-active';
|
const hoverClass = 'drag-zone-hover';
|
||||||
|
|
||||||
function ensureBranchOrder() {
|
|
||||||
branches.forEach((branch, i) => {
|
|
||||||
if (branch.order !== i) {
|
|
||||||
virtualBranches.updateBranchOrder(branch.id, i);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDndEvent(e: CustomEvent<DndEvent<Branch>>) {
|
|
||||||
branches = e.detail.items;
|
|
||||||
|
|
||||||
if (e.type == 'finalize') {
|
|
||||||
branches = branches.filter((branch) => branch.active);
|
|
||||||
ensureBranchOrder();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEmpty() {
|
function handleEmpty() {
|
||||||
const emptyIndex = branches.findIndex((item) => !item.files || item.files.length == 0);
|
const emptyIndex = branches.findIndex((item) => !item.files || item.files.length == 0);
|
||||||
@ -40,22 +25,64 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
bind:this={dropZone}
|
||||||
id="branch-lanes"
|
id="branch-lanes"
|
||||||
class="flex max-w-full flex-shrink flex-grow snap-x items-start overflow-x-auto overflow-y-hidden bg-light-200 px-2 dark:bg-dark-1000"
|
class="flex max-w-full flex-shrink flex-grow snap-x items-start overflow-x-auto overflow-y-hidden bg-light-200 px-2 dark:bg-dark-1000"
|
||||||
use:dndzone={{
|
on:dragenter={(e) => {
|
||||||
items: branches,
|
if (!e.dataTransfer?.types.includes('text/branch')) {
|
||||||
types: ['branch'],
|
return;
|
||||||
receives: ['branch'],
|
}
|
||||||
dropTargetClassMap: {
|
dropZone.classList.add(hoverClass);
|
||||||
file: [newBranchClass],
|
}}
|
||||||
hunk: [newBranchClass]
|
on:dragend={(e) => {
|
||||||
|
if (!e.dataTransfer?.types.includes('text/branch')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dropZone.classList.remove(hoverClass);
|
||||||
|
}}
|
||||||
|
on:dragover={(e) => {
|
||||||
|
if (!e.dataTransfer?.types.includes('text/branch')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault(); // Only when text/branch
|
||||||
|
const children = [...e.currentTarget.children];
|
||||||
|
dropPosition = 0;
|
||||||
|
for (let i = 0; i < children.length; i++) {
|
||||||
|
const pos = children[i].getBoundingClientRect();
|
||||||
|
if (e.clientX > pos.left + pos.width) {
|
||||||
|
dropPosition = i + 1; // Note that this is declared in the <script>
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const idx = children.indexOf(dragged);
|
||||||
|
if (idx != dropPosition) {
|
||||||
|
idx >= dropPosition
|
||||||
|
? children[dropPosition].before(dragged)
|
||||||
|
: children[dropPosition].after(dragged);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
on:drop={(e) => {
|
||||||
|
dropZone.classList.remove(hoverClass);
|
||||||
|
if (priorPosition != dropPosition) {
|
||||||
|
const el = branches.splice(priorPosition, 1);
|
||||||
|
branches.splice(dropPosition, 0, ...el);
|
||||||
|
branches.forEach((branch, i) => {
|
||||||
|
if (branch.order !== i) {
|
||||||
|
virtualBranches.updateBranchOrder(branch.id, i);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
on:consider={handleDndEvent}
|
|
||||||
on:finalize={handleDndEvent}
|
|
||||||
>
|
>
|
||||||
{#each branches.filter((c) => c.active) as { id, name, files, commits, upstream, description, order } (id)}
|
{#each branches.filter((c) => c.active) as { id, name, files, commits, upstream, description, order } (id)}
|
||||||
<Lane
|
<Lane
|
||||||
|
on:dragstart={(e) => {
|
||||||
|
if (!e.dataTransfer) return;
|
||||||
|
e.dataTransfer.setData('text/branch', id);
|
||||||
|
dragged = e.currentTarget;
|
||||||
|
priorPosition = Array.from(dropZone.children).indexOf(dragged);
|
||||||
|
}}
|
||||||
{name}
|
{name}
|
||||||
commitMessage={description}
|
commitMessage={description}
|
||||||
{files}
|
{files}
|
||||||
@ -73,7 +100,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
:global(#branch-lanes.new-branch-active [data-dnd-ignore]) {
|
:global(.drag-zone-hover *) {
|
||||||
@apply visible flex;
|
@apply pointer-events-none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
const zones = new Set<HTMLDivElement>();
|
||||||
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { dndzone } from 'svelte-dnd-action';
|
import type { Commit, File, Hunk } from '$lib/api/ipc/vbranches';
|
||||||
import type { DndEvent } from 'svelte-dnd-action/typings';
|
|
||||||
import { Commit, File, Hunk } from '$lib/api/ipc/vbranches';
|
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
import { createFile } from './helpers';
|
|
||||||
import FileCard from './FileCard.svelte';
|
import FileCard from './FileCard.svelte';
|
||||||
import { IconBranch } from '$lib/icons';
|
import { IconBranch } from '$lib/icons';
|
||||||
import { Button } from '$lib/components';
|
import { Button } from '$lib/components';
|
||||||
@ -39,26 +40,15 @@
|
|||||||
let isPushing = false;
|
let isPushing = false;
|
||||||
let popupMenu: PopupMenu;
|
let popupMenu: PopupMenu;
|
||||||
let meatballButton: HTMLButtonElement;
|
let meatballButton: HTMLButtonElement;
|
||||||
|
let dropZone: HTMLDivElement;
|
||||||
|
|
||||||
function handleDndEvent(e: CustomEvent<DndEvent<File | Hunk>>) {
|
const hoverClass = 'drag-zone-hover';
|
||||||
const newItems = e.detail.items;
|
const hunkType = 'text/hunk';
|
||||||
const fileItems = newItems.filter((item) => item instanceof File) as File[];
|
|
||||||
|
|
||||||
console.log('lane: handleDndEvent', e.type, e.detail.items);
|
onMount(() => {
|
||||||
|
zones.add(dropZone);
|
||||||
const hunkItems = newItems.filter((item) => item instanceof Hunk) as Hunk[];
|
return () => zones.delete(dropZone);
|
||||||
hunkItems.forEach((hunk) => {
|
});
|
||||||
const file = files.find((f) => f.hunks.find((h) => h.id == hunk.id));
|
|
||||||
if (file) {
|
|
||||||
file.hunks.push(hunk);
|
|
||||||
} else {
|
|
||||||
fileItems.push(createFile(hunk.filePath, hunk));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
files = fileItems.filter((file) => file.hunks && file.hunks.length > 0);
|
|
||||||
if (e.type === 'finalize') updateBranchOwnership();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateBranchOwnership() {
|
function updateBranchOwnership() {
|
||||||
const ownership = files
|
const ownership = files
|
||||||
@ -133,12 +123,53 @@
|
|||||||
console.log('branch name change:', name);
|
console.log('branch name change:', name);
|
||||||
virtualBranches.updateBranchName(branchId, name);
|
virtualBranches.updateBranchName(branchId, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isChildOf(child: any, parent: HTMLElement): boolean {
|
||||||
|
if (parent === child) return false;
|
||||||
|
if (!child.parentElement) return false;
|
||||||
|
if (child.parentElement == parent) return true;
|
||||||
|
return isChildOf(child.parentElement, parent);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex max-h-full min-w-[24rem] max-w-[120ch] shrink-0 snap-center flex-col overflow-y-auto py-2 px-3 transition-width dark:text-dark-100"
|
draggable="true"
|
||||||
|
bind:this={dropZone}
|
||||||
|
class="flex max-h-full min-w-[24rem] max-w-[120ch] shrink-0 snap-center flex-col overflow-y-auto bg-light-200 py-2 px-3 transition-width dark:bg-dark-1000 dark:text-dark-100"
|
||||||
class:w-full={maximized}
|
class:w-full={maximized}
|
||||||
class:w-96={!maximized}
|
class:w-96={!maximized}
|
||||||
|
on:dragstart
|
||||||
|
on:dragenter={(e) => {
|
||||||
|
if (!e.dataTransfer?.types.includes(hunkType)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dropZone.classList.add(hoverClass);
|
||||||
|
}}
|
||||||
|
on:dragleave|stopPropagation={(e) => {
|
||||||
|
if (!e.dataTransfer?.types.includes(hunkType)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isChildOf(e.target, dropZone)) {
|
||||||
|
dropZone.classList.remove(hoverClass);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
on:dragover|stopPropagation={(e) => {
|
||||||
|
if (e.dataTransfer?.types.includes(hunkType)) e.preventDefault();
|
||||||
|
}}
|
||||||
|
on:dragend={(e) => {
|
||||||
|
dropZone.classList.remove(hoverClass);
|
||||||
|
}}
|
||||||
|
on:drop|stopPropagation={(e) => {
|
||||||
|
if (!e.dataTransfer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dropZone.classList.remove(hoverClass);
|
||||||
|
const data = e.dataTransfer.getData(hunkType);
|
||||||
|
const ownership = files
|
||||||
|
.map((file) => file.id + ':' + file.hunks.map((hunk) => hunk.id).join(','))
|
||||||
|
.join('\n');
|
||||||
|
virtualBranches.updateBranchOwnership(branchId, (data + '\n' + ownership).trim());
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mb-2 flex w-full shrink-0 items-center gap-x-2 rounded-lg bg-light-200 text-lg font-bold text-light-900 dark:bg-dark-1000 dark:font-normal dark:text-dark-100"
|
class="mb-2 flex w-full shrink-0 items-center gap-x-2 rounded-lg bg-light-200 text-lg font-bold text-light-900 dark:bg-dark-1000 dark:font-normal dark:text-dark-100"
|
||||||
@ -194,39 +225,29 @@
|
|||||||
class="flex flex-col rounded bg-white p-2 shadow-lg dark:border dark:border-dark-600 dark:bg-dark-800"
|
class="flex flex-col rounded bg-white p-2 shadow-lg dark:border dark:border-dark-600 dark:bg-dark-800"
|
||||||
>
|
>
|
||||||
<div class="mb-2 flex items-center">
|
<div class="mb-2 flex items-center">
|
||||||
{#if files.filter((x) => x.hunks).length > 0}
|
<textarea
|
||||||
<textarea
|
bind:value={commitMessage}
|
||||||
bind:value={commitMessage}
|
class="shrink-0 flex-grow cursor-text resize-none overflow-x-auto overflow-y-auto rounded border border-white bg-white p-2 text-dark-700 outline-none hover:border-light-400 focus:border-light-400 focus:ring-0 dark:border-dark-500 dark:bg-dark-700 dark:text-light-400 dark:hover:border-dark-300 dark:focus:border-dark-300"
|
||||||
class="shrink-0 flex-grow cursor-text resize-none overflow-x-auto overflow-y-auto rounded border border-white bg-white p-2 text-dark-700 outline-none hover:border-light-400 focus:border-light-400 focus:ring-0 dark:border-dark-500 dark:bg-dark-700 dark:text-light-400 dark:hover:border-dark-300 dark:focus:border-dark-300"
|
placeholder="Your commit message here..."
|
||||||
placeholder="Your commit message here..."
|
rows={messageRows}
|
||||||
rows={messageRows}
|
/>
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4 text-right">
|
<div class="mb-4 text-right">
|
||||||
{#if files.filter((x) => x.hunks).length > 0}
|
<Button
|
||||||
<Button
|
height="small"
|
||||||
height="small"
|
color="purple"
|
||||||
color="purple"
|
on:click={() => {
|
||||||
on:click={() => {
|
commit();
|
||||||
commit();
|
}}>Commit</Button
|
||||||
}}>Commit</Button
|
>
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="flex flex-shrink flex-col gap-y-2">
|
||||||
class="flex flex-shrink flex-col gap-y-2"
|
<div class="drag-zone-marker hidden rounded-lg border p-6">
|
||||||
use:dndzone={{
|
Drop here to add to virtual branch
|
||||||
items: files,
|
</div>
|
||||||
zoneTabIndex: -1,
|
{#each files as file (file.id)}
|
||||||
types: ['file'],
|
|
||||||
receives: ['file', 'hunk']
|
|
||||||
}}
|
|
||||||
on:consider={handleDndEvent}
|
|
||||||
on:finalize={handleDndEvent}
|
|
||||||
>
|
|
||||||
{#each files.filter((x) => x.hunks) as file (file.id)}
|
|
||||||
<FileCard
|
<FileCard
|
||||||
|
id={file.id}
|
||||||
filepath={file.path}
|
filepath={file.path}
|
||||||
expanded={file.expanded}
|
expanded={file.expanded}
|
||||||
hunks={file.hunks}
|
hunks={file.hunks}
|
||||||
@ -238,11 +259,21 @@
|
|||||||
setExpandedWithCache(file, e.detail);
|
setExpandedWithCache(file, e.detail);
|
||||||
expandFromCache();
|
expandFromCache();
|
||||||
}}
|
}}
|
||||||
|
on:drag={(e) => {
|
||||||
|
zones.forEach((zone) => {
|
||||||
|
if (zone != dropZone) {
|
||||||
|
e.detail
|
||||||
|
? zone.classList.add('drag-zone-active')
|
||||||
|
: zone.classList.remove('drag-zone-active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
{projectPath}
|
{projectPath}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
{#if files.filter((x) => x.hunks).length == 0}
|
{#if files.length == 0}
|
||||||
<div class="p-3 pt-0">No uncommitted work on this branch.</div>
|
<!-- attention: these markers have custom css at the bottom of thise file -->
|
||||||
|
<div class="no-changes p-2" data-dnd-ignore>No uncommitted work on this branch.</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { dndzone } from 'svelte-dnd-action';
|
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import type { DndEvent } from 'svelte-dnd-action/typings';
|
|
||||||
import type { Hunk } from '$lib/api/ipc/vbranches';
|
import type { Hunk } from '$lib/api/ipc/vbranches';
|
||||||
import HunkDiffViewer from './HunkDiffViewer.svelte';
|
import HunkDiffViewer from './HunkDiffViewer.svelte';
|
||||||
import { summarizeHunk } from '$lib/summaries';
|
import { summarizeHunk } from '$lib/summaries';
|
||||||
@ -11,6 +9,7 @@
|
|||||||
import PopupMenu from '$lib/components/PopupMenu/PopupMenu.svelte';
|
import PopupMenu from '$lib/components/PopupMenu/PopupMenu.svelte';
|
||||||
import PopupMenuItem from '$lib/components/PopupMenu/PopupMenuItem.svelte';
|
import PopupMenuItem from '$lib/components/PopupMenu/PopupMenuItem.svelte';
|
||||||
|
|
||||||
|
export let id: string;
|
||||||
export let projectPath: string;
|
export let projectPath: string;
|
||||||
export let filepath: string;
|
export let filepath: string;
|
||||||
export let hunks: Hunk[];
|
export let hunks: Hunk[];
|
||||||
@ -19,16 +18,12 @@
|
|||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
update: Hunk[];
|
update: Hunk[];
|
||||||
|
drag: boolean;
|
||||||
}>();
|
}>();
|
||||||
export let expanded: boolean | undefined;
|
export let expanded: boolean | undefined;
|
||||||
|
|
||||||
let popupMenu: PopupMenu;
|
let popupMenu: PopupMenu;
|
||||||
|
|
||||||
function handleDndEvent(e: CustomEvent<DndEvent<Hunk>>) {
|
|
||||||
hunks = e.detail.items;
|
|
||||||
if (e.type == 'finalize') dispatch('update', e.detail.items);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hunkSize(hunk: string): number[] {
|
function hunkSize(hunk: string): number[] {
|
||||||
const linesAdded = hunk.split('\n').filter((line) => line.startsWith('+')).length;
|
const linesAdded = hunk.split('\n').filter((line) => line.startsWith('+')).length;
|
||||||
const linesRemoved = hunk.split('\n').filter((line) => line.startsWith('-')).length;
|
const linesRemoved = hunk.split('\n').filter((line) => line.startsWith('-')).length;
|
||||||
@ -53,9 +48,19 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
draggable="true"
|
||||||
|
on:dragstart|stopPropagation={(e) => {
|
||||||
|
if (!e.dataTransfer) return;
|
||||||
|
e.dataTransfer.setData('text/hunk', id + ':' + hunks.map((h) => h.id).join(','));
|
||||||
|
dispatch('drag', true);
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
on:dragend|stopPropagation={(e) => {
|
||||||
|
dispatch('drag', false);
|
||||||
|
}}
|
||||||
class="changed-file flex w-full flex-col justify-center gap-2 rounded-lg border border-light-300 bg-light-50 text-light-900 dark:border-dark-400 dark:bg-dark-700 dark:text-light-300"
|
class="changed-file flex w-full flex-col justify-center gap-2 rounded-lg border border-light-300 bg-light-50 text-light-900 dark:border-dark-400 dark:bg-dark-700 dark:text-light-300"
|
||||||
>
|
>
|
||||||
<div class="flex items-center px-2 pt-2">
|
<div class="items-cente flex px-2 pt-2">
|
||||||
<div class="flex-grow overflow-hidden text-ellipsis whitespace-nowrap " title={filepath}>
|
<div class="flex-grow overflow-hidden text-ellipsis whitespace-nowrap " title={filepath}>
|
||||||
{@html boldenFilename(filepath)}
|
{@html boldenFilename(filepath)}
|
||||||
</div>
|
</div>
|
||||||
@ -75,23 +80,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="hunk-change-container flex flex-col gap-2 rounded px-2 pb-2">
|
||||||
class="hunk-change-container flex flex-col gap-2 rounded px-2 pb-2"
|
|
||||||
use:dndzone={{
|
|
||||||
items: hunks,
|
|
||||||
zoneTabIndex: -1,
|
|
||||||
autoAriaDisabled: true,
|
|
||||||
types: ['hunk', filepath],
|
|
||||||
receives: [filepath]
|
|
||||||
}}
|
|
||||||
on:consider={handleDndEvent}
|
|
||||||
on:finalize={handleDndEvent}
|
|
||||||
>
|
|
||||||
{#if expanded}
|
{#if expanded}
|
||||||
{#each hunks || [] as hunk (hunk.id)}
|
{#each hunks || [] as hunk (hunk.id)}
|
||||||
<div
|
<div
|
||||||
class="changed-hunk flex w-full flex-col rounded-lg border border-light-200 bg-white dark:border-dark-400 dark:bg-dark-900"
|
draggable="true"
|
||||||
|
on:dragstart|stopPropagation={(e) => {
|
||||||
|
if (!e.dataTransfer) return;
|
||||||
|
e.dataTransfer.setData('text/hunk', id + ':' + hunk.id);
|
||||||
|
dispatch('drag', true);
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
on:dragend|stopPropagation={(e) => {
|
||||||
|
dispatch('drag', false);
|
||||||
|
}}
|
||||||
on:contextmenu|preventDefault={(e) => popupMenu.openByMouse(e, hunk)}
|
on:contextmenu|preventDefault={(e) => popupMenu.openByMouse(e, hunk)}
|
||||||
|
class="changed-hunk flex w-full flex-col rounded-lg border border-light-200 bg-white dark:border-dark-400 dark:bg-dark-900"
|
||||||
>
|
>
|
||||||
<div class="truncate whitespace-normal p-2">
|
<div class="truncate whitespace-normal p-2">
|
||||||
{#await summarizeHunk(hunk.diff) then description}
|
{#await summarizeHunk(hunk.diff) then description}
|
||||||
|
@ -1,72 +1,58 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { dndzone } from 'svelte-dnd-action';
|
import type { Branch } from '$lib/api/ipc/vbranches';
|
||||||
import { Branch, File, Hunk } from '$lib/api/ipc/vbranches';
|
|
||||||
import type { DndEvent } from 'svelte-dnd-action/typings';
|
|
||||||
import { createBranch, createFile } from './helpers';
|
|
||||||
import { Button } from '$lib/components';
|
import { Button } from '$lib/components';
|
||||||
import type { VirtualBranchOperations } from './vbranches';
|
import type { VirtualBranchOperations } from './vbranches';
|
||||||
|
|
||||||
export let virtualBranches: VirtualBranchOperations;
|
export let virtualBranches: VirtualBranchOperations;
|
||||||
let items: Branch[] = [];
|
let items: Branch[] = [];
|
||||||
|
let dropZone: HTMLDivElement;
|
||||||
|
|
||||||
function handleNewVirtualBranch() {
|
function handleNewVirtualBranch() {
|
||||||
virtualBranches.createBranch({});
|
virtualBranches.createBranch({});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDndFinalize(e: CustomEvent<DndEvent<Branch | File | Hunk>>) {
|
function isChildOf(child: any, parent: HTMLElement): boolean {
|
||||||
console.log('new dropzone: handleDndFinalize', e.type, e.detail.items);
|
if (parent === child) return false;
|
||||||
const newItems = e.detail.items;
|
if (!child.parentElement) return false;
|
||||||
const branchItems = newItems.filter((item) => item instanceof Branch) as Branch[];
|
if (child.parentElement == parent) return true;
|
||||||
|
return isChildOf(child.parentElement, parent);
|
||||||
const hunkItems = newItems.filter((item) => item instanceof Hunk) as Hunk[];
|
|
||||||
for (const hunk of hunkItems) {
|
|
||||||
branchItems.push(createBranch(createFile(hunk.filePath, hunk)));
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileItems = newItems.filter((item) => item instanceof File) as File[];
|
|
||||||
for (const file of fileItems) {
|
|
||||||
branchItems.push(createBranch(file));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.type == 'finalize') {
|
|
||||||
const ownership = branchItems[0].files
|
|
||||||
.map((file) => file.id + ':' + file.hunks.map((hunk) => hunk.id).join(','))
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
virtualBranches.createBranch({ ownership });
|
|
||||||
items = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
items = branchItems;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="new-branch-dz"
|
id="new-branch-dz"
|
||||||
class="ml-4 mt-16 flex h-40 w-[22.5rem] shrink-0 items-center rounded-lg border border-dashed border-light-600 px-8 py-10"
|
class="h-42 ml-4 mt-16 flex w-[22.5rem] shrink-0 justify-center text-center text-light-800 dark:text-dark-100"
|
||||||
use:dndzone={{
|
bind:this={dropZone}
|
||||||
items: items,
|
on:dragover|stopPropagation={(e) => {
|
||||||
types: ['new-branch'],
|
if (e.dataTransfer?.types.includes('text/hunk')) e.preventDefault();
|
||||||
receives: ['file', 'hunk'],
|
dropZone.classList.add('drag-zone-hover');
|
||||||
dropTargetClassMap: {
|
}}
|
||||||
file: ['new-branch-active'],
|
on:dragleave|stopPropagation={(e) => {
|
||||||
hunk: ['new-branch-active']
|
if (!isChildOf(e.target, dropZone)) {
|
||||||
|
dropZone.classList.remove('drag-zone-hover');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
on:finalize={handleDndFinalize}
|
on:drop|stopPropagation={(e) => {
|
||||||
|
if (!e.dataTransfer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dropZone.classList.remove('drag-zone-hover');
|
||||||
|
const ownership = e.dataTransfer.getData('text/hunk');
|
||||||
|
virtualBranches.createBranch({ ownership });
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div class="bg-green-300" />
|
||||||
class="flex flex-col items-center gap-y-3 self-center p-8 text-center text-lg text-light-800 dark:text-dark-100"
|
<div class="call-to-action flex-grow rounded-lg border border-dashed border-light-600 p-8">
|
||||||
>
|
<div class="flex flex-col items-center gap-y-3 self-center p-2">
|
||||||
<p>Drag changes or click button to create new virtual branch</p>
|
<p>Drag changes or click button to create new virtual branch</p>
|
||||||
<Button color="purple" height="small" on:click={handleNewVirtualBranch}
|
<Button color="purple" height="small" on:click={handleNewVirtualBranch}
|
||||||
>New virtual branch</Button
|
>New virtual branch</Button
|
||||||
>
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="drag-zone-marker hidden flex-grow rounded-lg border border-green-450 p-8">
|
||||||
|
<div class="flex flex-col items-center gap-y-3 self-center p-2">
|
||||||
|
<p>Drop here to add to virtual branch</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
|
||||||
:global(#new-branch-dz.new-branch-active) {
|
|
||||||
@apply visible flex;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -108,7 +108,12 @@ const config = {
|
|||||||
900: '#7c2d12'
|
900: '#7c2d12'
|
||||||
},
|
},
|
||||||
green: {
|
green: {
|
||||||
|
200: '#AFEDB1',
|
||||||
|
300: '#6BE66D',
|
||||||
400: '#4ade80',
|
400: '#4ade80',
|
||||||
|
450: '#40C341',
|
||||||
|
460: '#346E45',
|
||||||
|
470: '#314D39',
|
||||||
500: '#22c55e',
|
500: '#22c55e',
|
||||||
600: '#16a34a',
|
600: '#16a34a',
|
||||||
700: '#15803d',
|
700: '#15803d',
|
||||||
|
Loading…
Reference in New Issue
Block a user