mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-30 11:32:04 +03:00
Improve nested drag & drop functionality
- wait until dropped before removing wrapped containers - propagate empty events up the compenents tree
This commit is contained in:
parent
baa0bc8978
commit
707707ba52
@ -6,7 +6,7 @@ import { subSeconds, subMinutes, subHours } from 'date-fns';
|
||||
export const load: PageLoad = async () => {
|
||||
const branches: Branch[] = [
|
||||
{
|
||||
id: 'c1',
|
||||
id: 'b1',
|
||||
name: 'feature-1',
|
||||
active: true,
|
||||
kind: 'branch',
|
||||
@ -93,8 +93,8 @@ export const load: PageLoad = async () => {
|
||||
kind: 'branch',
|
||||
commits: [
|
||||
{
|
||||
id: 'c2',
|
||||
description: 'Second commit',
|
||||
id: 'c3',
|
||||
description: 'Third commit',
|
||||
committedAt: subMinutes(new Date(), 50),
|
||||
kind: 'commit',
|
||||
files: [
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { flip } from 'svelte/animate';
|
||||
import { dndzone } from 'svelte-dnd-action';
|
||||
import Lane from './BranchLane.svelte';
|
||||
import type { Branch, File, Hunk } from './types';
|
||||
import type { Branch, Commit, File, Hunk } from './types';
|
||||
import type { DndEvent } from 'svelte-dnd-action/typings';
|
||||
|
||||
export let branches: Branch[];
|
||||
@ -10,30 +10,92 @@
|
||||
const flipDurationMs = 300;
|
||||
|
||||
function handleDndEvent(
|
||||
e: CustomEvent<DndEvent<Branch | File | Hunk>> & { target: HTMLElement }
|
||||
e: CustomEvent<DndEvent<Branch | Commit | File | Hunk>>,
|
||||
isFinal: boolean
|
||||
) {
|
||||
branches = e.detail.items.filter((item) => item.kind == 'branch') as Branch[];
|
||||
// TODO: Create lanes out of dropped files/hunks
|
||||
const branchItems = e.detail.items.filter((item) => item.kind == 'branch') as Branch[];
|
||||
const commitItems = e.detail.items.filter((item) => item.kind == 'commit') as Commit[];
|
||||
const fileItems = e.detail.items.filter((item) => item.kind == 'file') as File[];
|
||||
const hunkItems = e.detail.items.filter((item) => item.kind == 'hunk') as Hunk[];
|
||||
|
||||
for (const hunk of hunkItems) {
|
||||
branchItems.push({
|
||||
id: `${Date.now()}-${hunk.id}-branch`,
|
||||
name: 'new branch',
|
||||
active: true,
|
||||
kind: 'branch',
|
||||
commits: [
|
||||
{
|
||||
id: `${Date.now()}-${hunk.id}-commit`,
|
||||
description: 'New commit',
|
||||
kind: 'commit',
|
||||
files: [
|
||||
{
|
||||
id: `${Date.now()}-${hunk.id}-hunk`,
|
||||
path: hunk.filePath,
|
||||
kind: 'file',
|
||||
hunks: [{ ...hunk, isDndShadowItem: !isFinal }]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
for (const file of fileItems) {
|
||||
branchItems.push({
|
||||
id: `${Date.now()}-${file.id}-branch`,
|
||||
name: 'new branch',
|
||||
active: true,
|
||||
kind: 'branch',
|
||||
commits: [
|
||||
{
|
||||
id: `${Date.now()}-${file.id}-commit`,
|
||||
description: '',
|
||||
kind: 'commit',
|
||||
files: [file],
|
||||
isDndShadowItem: !isFinal
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
for (const commit of commitItems) {
|
||||
branchItems.push({
|
||||
id: `${Date.now()}-${commit.id}-branch`,
|
||||
name: 'new branch',
|
||||
kind: 'branch',
|
||||
active: true,
|
||||
commits: [commit],
|
||||
isDndShadowItem: !isFinal
|
||||
});
|
||||
}
|
||||
branches = branchItems.filter((commit) => commit.active);
|
||||
}
|
||||
|
||||
function handleEmpty() {
|
||||
const emptyIndex = branches.findIndex((item) => !item.commits || item.commits.length == 0);
|
||||
if (emptyIndex != -1) {
|
||||
// TODO: Figure out what to do when a branch is empty. Just removing it is a bit jarring.
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="flex gap-x-4 p-4"
|
||||
class="flex w-full gap-x-8 p-8"
|
||||
use:dndzone={{
|
||||
items: branches,
|
||||
flipDurationMs,
|
||||
types: ['branch'],
|
||||
receives: ['branch', 'file', 'hunk']
|
||||
receives: ['branch', 'commit', 'file', 'hunk']
|
||||
}}
|
||||
on:consider={handleDndEvent}
|
||||
on:finalize={handleDndEvent}
|
||||
on:consider={(e) => handleDndEvent(e, false)}
|
||||
on:finalize={(e) => handleDndEvent(e, true)}
|
||||
>
|
||||
{#each branches.filter((c) => c.active) as { id, name, commits }, idx (id)}
|
||||
<div
|
||||
class="flex w-64 border border-zinc-700 bg-zinc-900/50 p-4"
|
||||
animate:flip={{ duration: flipDurationMs }}
|
||||
>
|
||||
<Lane {name} bind:commits />
|
||||
<Lane {name} bind:commits on:empty={handleEmpty} />
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
|
@ -4,17 +4,49 @@
|
||||
import type { DndEvent } from 'svelte-dnd-action/typings';
|
||||
import type { Commit, File, Hunk } from './types';
|
||||
import CommitGroup from './CommitGroup.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let name: string;
|
||||
export let commits: Commit[];
|
||||
|
||||
const flipDurationMs = 150;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function handleDndEvent(
|
||||
e: CustomEvent<DndEvent<Commit | File | Hunk>> & { target: HTMLElement }
|
||||
) {
|
||||
commits = e.detail.items.filter((item) => item.kind == 'commit') as Commit[];
|
||||
// TODO: Create lanes out of dropped files/hunks
|
||||
function handleDndEvent(e: CustomEvent<DndEvent<Commit | File | Hunk>>, isFinal: boolean) {
|
||||
const commitItems = e.detail.items.filter((item) => item.kind == 'commit') as Commit[];
|
||||
const fileItems = e.detail.items.filter((item) => item.kind == 'file') as File[];
|
||||
const hunkItems = e.detail.items.filter((item) => item.kind == 'hunk') as Hunk[];
|
||||
|
||||
// Merge hunks into existing files, or create new where none exist
|
||||
for (const hunk of hunkItems) {
|
||||
commitItems.push({
|
||||
id: `${Date.now()}-${hunk.id}-commit`,
|
||||
description: 'New commit',
|
||||
kind: 'commit',
|
||||
files: [
|
||||
{
|
||||
id: `${Date.now()}-${hunk.id}-hunk`,
|
||||
path: hunk.filePath,
|
||||
kind: 'file',
|
||||
hunks: [{ ...hunk, isDndShadowItem: !isFinal }]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
for (const file of fileItems) {
|
||||
commitItems.push({
|
||||
id: `${Date.now()}-${file.id}`,
|
||||
description: '',
|
||||
kind: 'commit',
|
||||
files: [file],
|
||||
isDndShadowItem: !isFinal
|
||||
});
|
||||
}
|
||||
commits = commitItems.filter((commit) => commit.files && commit.files.length > 0);
|
||||
|
||||
if (e.type == 'finalize' && (!commits || commits.length == 0)) {
|
||||
dispatch('empty');
|
||||
}
|
||||
}
|
||||
|
||||
function handleEmpty() {
|
||||
@ -22,6 +54,9 @@
|
||||
if (emptyIndex != -1) {
|
||||
commits.splice(emptyIndex, 1);
|
||||
}
|
||||
if (commits.length == 0) {
|
||||
dispatch('empty');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -36,10 +71,10 @@
|
||||
flipDurationMs,
|
||||
zoneTabIndex: -1,
|
||||
types: ['commit'],
|
||||
receives: ['commit']
|
||||
receives: ['commit', 'file', 'hunk']
|
||||
}}
|
||||
on:consider={(e) => handleDndEvent(e)}
|
||||
on:finalize={(e) => handleDndEvent(e)}
|
||||
on:consider={(e) => handleDndEvent(e, false)}
|
||||
on:finalize={(e) => handleDndEvent(e, true)}
|
||||
>
|
||||
{#each commits.filter((x) => x.files) as { id, description, files }, idx (id)}
|
||||
<div animate:flip={{ duration: flipDurationMs }}>
|
||||
|
@ -2,14 +2,16 @@
|
||||
import { flip } from 'svelte/animate';
|
||||
import { dndzone } from 'svelte-dnd-action';
|
||||
import type { DndEvent } from 'svelte-dnd-action/typings';
|
||||
import type { Commit, File, Hunk } from './types';
|
||||
import type { File, Hunk } from './types';
|
||||
import FileCard from './FileCard.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let description: string;
|
||||
export let id: string;
|
||||
export let files: File[];
|
||||
|
||||
const flipDurationMs = 150;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function handleDndEvent(e: CustomEvent<DndEvent<File | Hunk>>, isFinal: boolean) {
|
||||
const fileItems = e.detail.items.filter((item) => item.kind == 'file') as File[];
|
||||
@ -25,27 +27,26 @@
|
||||
id: `${Date.now()}-${hunk.id}`,
|
||||
path: hunk.filePath,
|
||||
kind: 'file',
|
||||
hunks: [
|
||||
{
|
||||
id: hunk.id,
|
||||
filePath: hunk.filePath,
|
||||
kind: hunk.kind,
|
||||
modifiedAt: hunk.modifiedAt,
|
||||
name: hunk.name,
|
||||
isDndShadowItem: !isFinal
|
||||
}
|
||||
]
|
||||
hunks: [{ ...hunk, isDndShadowItem: !isFinal }]
|
||||
});
|
||||
}
|
||||
}
|
||||
files = fileItems.filter((file) => file.hunks && file.hunks.length > 0);
|
||||
|
||||
if (e.type == 'finalize' && (!files || files.length == 0)) {
|
||||
dispatch('empty');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEmptyFile() {
|
||||
function handleEmpty() {
|
||||
const emptyIndex = files.findIndex((item) => !item.hunks || item.hunks.length == 0);
|
||||
if (emptyIndex != -1) {
|
||||
files.splice(emptyIndex, 1);
|
||||
}
|
||||
if (files.length == 0) {
|
||||
dispatch('empty');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -64,7 +65,7 @@
|
||||
>
|
||||
{#each files.filter((x) => x.hunks) as file, idx (file.id)}
|
||||
<div animate:flip={{ duration: flipDurationMs }}>
|
||||
<FileCard bind:file on:empty={handleEmptyFile} />
|
||||
<FileCard bind:file on:empty={handleEmpty} />
|
||||
</div>
|
||||
{/each}
|
||||
<div>
|
||||
|
@ -13,12 +13,12 @@
|
||||
const flipDurationMs = 150;
|
||||
|
||||
function handleDndEvent(e: CustomEvent<DndEvent<Hunk>>) {
|
||||
if (!file.hunks || file.hunks.length == 0) {
|
||||
dispatch('empty');
|
||||
return;
|
||||
}
|
||||
e.detail.items.sort((itemA, itemB) => compareDesc(itemA.modifiedAt, itemB.modifiedAt));
|
||||
file.hunks = e.detail.items;
|
||||
|
||||
if (e.type == 'finalize' && (!file.hunks || file.hunks.length == 0)) {
|
||||
dispatch('empty');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -18,9 +18,10 @@ export type File = {
|
||||
export type Commit = {
|
||||
id: string;
|
||||
description: string;
|
||||
committedAt: Date;
|
||||
committedAt?: Date;
|
||||
files: File[];
|
||||
kind: string;
|
||||
isDndShadowItem?: boolean;
|
||||
};
|
||||
|
||||
export type Branch = {
|
||||
@ -29,4 +30,5 @@ export type Branch = {
|
||||
active: boolean;
|
||||
commits: Commit[];
|
||||
kind: string;
|
||||
isDndShadowItem?: boolean;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user