Update drag & drop to match forked library

- svelte-dnd-action now installed from gitbutlerapp fork
- dnd zones can have multiple types and require receivable types
- dropped items that do not match zone have to be wrapped in handlers
This commit is contained in:
Mattias Granlund 2023-06-12 15:33:47 +02:00 committed by Kiril Videlov
parent d21d7e4909
commit dbcb391252
9 changed files with 129 additions and 39 deletions

View File

@ -72,7 +72,7 @@
"prettier-plugin-tailwindcss": "^0.3.0",
"svelte": "~3.55.1",
"svelte-check": "^3.0.1",
"svelte-dnd-action": "^0.9.22",
"svelte-dnd-action": "github:gitbutlerapp/svelte-dnd-action#fbe6a192bc99dd69d2d1946fefb2df9cea7b2de9",
"svelte-floating-ui": "^1.5.2",
"svelte-french-toast": "^1.0.3",
"svelte-loadable-store": "^1.2.3",

View File

@ -170,8 +170,8 @@ devDependencies:
specifier: ^3.0.1
version: 3.0.3(postcss-load-config@4.0.1)(postcss@8.4.21)(svelte@3.55.1)
svelte-dnd-action:
specifier: ^0.9.22
version: 0.9.22(svelte@3.55.1)
specifier: github:gitbutlerapp/svelte-dnd-action#fbe6a192bc99dd69d2d1946fefb2df9cea7b2de9
version: github.com/gitbutlerapp/svelte-dnd-action/fbe6a192bc99dd69d2d1946fefb2df9cea7b2de9(svelte@3.55.1)
svelte-floating-ui:
specifier: ^1.5.2
version: 1.5.2
@ -3686,14 +3686,6 @@ packages:
- sugarss
dev: true
/svelte-dnd-action@0.9.22(svelte@3.55.1):
resolution: {integrity: sha512-lOQJsNLM1QWv5mdxIkCVtk6k4lHCtLgfE59y8rs7iOM6erchbLC9hMEFYSveZz7biJV0mpg7yDSs4bj/RT/YkA==}
peerDependencies:
svelte: '>=3.23.0'
dependencies:
svelte: 3.55.1
dev: true
/svelte-eslint-parser@0.29.0(svelte@3.55.1):
resolution: {integrity: sha512-2uzOw9vRpSO3fo6NkbH7UynfCopQbMz/7LO9KT05YPvkB0uuFvFHex8+Ccv3gSrxHRvKS7FwJmV4H8WNWIzgWQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@ -4274,6 +4266,17 @@ packages:
engines: {node: '>=10'}
dev: true
github.com/gitbutlerapp/svelte-dnd-action/fbe6a192bc99dd69d2d1946fefb2df9cea7b2de9(svelte@3.55.1):
resolution: {tarball: https://codeload.github.com/gitbutlerapp/svelte-dnd-action/tar.gz/fbe6a192bc99dd69d2d1946fefb2df9cea7b2de9}
id: github.com/gitbutlerapp/svelte-dnd-action/fbe6a192bc99dd69d2d1946fefb2df9cea7b2de9
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/5e14c2cad7335a4284a6caad81d8cf37dd675a27:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/5e14c2cad7335a4284a6caad81d8cf37dd675a27}
name: tauri-plugin-log-api

2
src/global.d.ts vendored
View File

@ -1,5 +1,5 @@
declare type Item = import('svelte-dnd-action').Item;
declare type DndEvent<ItemType = Item> = import('svelte-dnd-action').DndEvent<ItemType>;
declare type DndEvent<ItemType = Item> = import('svelte-dnd-action/typings').DndEvent<ItemType>;
declare namespace svelte.JSX {
interface HTMLAttributes<T> {
onconsider?: (event: CustomEvent<DndEvent<ItemType>> & { target: EventTarget & T }) => void;

View File

@ -5,8 +5,6 @@
export let data: PageData;
let columnsData = data.columnsData;
$: console.log(columnsData);
</script>
<div class="flex h-full">

View File

@ -9,31 +9,40 @@ export const load: PageLoad = wrapLoadWithSentry(async () => {
id: 'c1',
name: 'feature-1',
active: true,
kind: 'lane',
files: [
{
id: 'f1',
path: 'src/foo.py',
kind: 'file',
hunks: [
{
id: 'h1',
name: 'foo-hunk-1',
modified: subMinutes(new Date(), 5)
kind: 'hunk',
modified: subMinutes(new Date(), 5),
filePath: 'src/foo.py'
},
{
id: 'h2',
name: 'foo-hunk-2',
modified: subSeconds(new Date(), 15)
kind: 'hunk',
modified: subSeconds(new Date(), 15),
filePath: 'src/foo.py'
}
]
},
{
id: 'f2',
path: 'src/bar.py',
kind: 'file',
hunks: [
{
id: 'h3',
name: 'bar-hunk-1',
modified: subHours(new Date(), 2)
kind: 'hunk',
modified: subHours(new Date(), 2),
filePath: 'src/bar.py'
}
]
}
@ -43,15 +52,19 @@ export const load: PageLoad = wrapLoadWithSentry(async () => {
id: 'c2',
name: 'bugfix',
active: true,
kind: 'lane',
files: [
{
id: 'f3',
path: 'src/foo.py',
kind: 'file',
hunks: [
{
id: 'h4',
name: 'foo-hunk-3',
modified: subMinutes(new Date(), 32)
kind: 'hunk',
modified: subMinutes(new Date(), 32),
filePath: 'src/foo.py'
}
]
}
@ -61,15 +74,19 @@ export const load: PageLoad = wrapLoadWithSentry(async () => {
id: 'c3',
name: 'stashed-things',
active: false,
kind: 'lane',
files: [
{
id: 'f4',
path: 'src/bar.py',
kind: 'file',
hunks: [
{
id: 'h5',
name: 'bar-hunk-2',
modified: subHours(new Date(), 1)
kind: 'hunk',
modified: subHours(new Date(), 1),
filePath: 'src/bar.py'
}
]
}

View File

@ -1,19 +1,32 @@
<script lang="ts">
import { flip } from 'svelte/animate';
import { dndzone } from 'svelte-dnd-action';
import type { BranchLane } from './board';
import Lane from './Lane.svelte';
import type { BranchLane, File, Hunk } from './board';
import type { DndEvent } from 'svelte-dnd-action/typings';
export let columns: BranchLane[];
const flipDurationMs = 300;
function handleDndEvent(
e: CustomEvent<DndEvent<BranchLane | File | Hunk>> & { target: HTMLElement }
) {
columns = e.detail.items.filter((item) => item.kind == 'lane') as BranchLane[];
// TODO: Create lanes out of dropped files/hunks
}
</script>
<section
class="flex gap-x-4 p-4"
use:dndzone={{ items: columns, flipDurationMs, type: 'column' }}
on:consider={(e) => (columns = e.detail.items)}
on:finalize={(e) => (columns = e.detail.items)}
use:dndzone={{
items: columns,
flipDurationMs,
types: ['lane'],
receives: ['lane', 'file', 'hunk']
}}
on:consider={handleDndEvent}
on:finalize={handleDndEvent}
>
{#each columns.filter((c) => c.active) as { id, name, files }, idx (id)}
<div

View File

@ -1,14 +1,22 @@
<script lang="ts">
import { flip } from 'svelte/animate';
import { createEventDispatcher } from 'svelte';
import { dndzone } from 'svelte-dnd-action';
import type { File, Hunk } from './board';
const flipDurationMs = 150;
import animateHeight from './animation';
import { flip } from 'svelte/animate';
import { formatDistanceToNow, compareDesc } from 'date-fns';
import animateHeight from './animation';
import type { DndEvent } from 'svelte-dnd-action/typings';
import type { File, Hunk } from './board';
export let file: File;
function sortAndUpdateHunks(e: { detail: { items: Hunk[] } }) {
const dispatch = createEventDispatcher();
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.modified, itemB.modified));
file.hunks = e.detail.items;
}
@ -28,13 +36,14 @@
items: file.hunks,
flipDurationMs,
zoneTabIndex: -1,
type: file.path,
autoAriaDisabled: true
autoAriaDisabled: true,
types: ['hunk', file.path],
receives: [file.path]
}}
on:consider={sortAndUpdateHunks}
on:finalize={sortAndUpdateHunks}
on:consider={handleDndEvent}
on:finalize={handleDndEvent}
>
{#each file.hunks as hunk (hunk.id)}
{#each file.hunks || [] as hunk (hunk.id)}
<div
animate:flip={{ duration: flipDurationMs }}
class="w-full rounded border border-zinc-500 bg-zinc-600 p-1"

View File

@ -1,13 +1,51 @@
<script lang="ts">
import { flip } from 'svelte/animate';
import { dndzone } from 'svelte-dnd-action';
import type { File } from './board';
import type { DndEvent } from 'svelte-dnd-action/typings';
import type { File, Hunk } from './board';
import FileCard from './FileCard.svelte';
export let name: string;
export let files: File[];
const flipDurationMs = 150;
function handleDndEvent(e: CustomEvent<DndEvent<File | Hunk>>, isFinal: boolean) {
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) {
const file = fileItems.find((file) => file.path == hunk.filePath);
if (file) {
file.hunks.push(hunk);
} else {
fileItems.push({
id: `${Date.now()}-${hunk.id}`,
path: hunk.filePath,
kind: 'file',
hunks: [
{
id: hunk.id,
filePath: hunk.filePath,
kind: hunk.kind,
modified: hunk.modified,
name: hunk.name,
isDndShadowItem: !isFinal
}
]
});
}
}
files = fileItems.filter((file) => file.hunks && file.hunks.length > 0);
}
function handleEmptyFile() {
const emptyIndex = files.findIndex((item) => !item.hunks || item.hunks.length == 0);
if (emptyIndex != -1) {
files.splice(emptyIndex, 1);
}
}
</script>
<div class="flex h-full w-full flex-col">
@ -16,13 +54,19 @@
</div>
<div
class="flex flex-grow flex-col gap-y-4"
use:dndzone={{ items: files, flipDurationMs, zoneTabIndex: -1 }}
on:consider={(e) => (files = e.detail.items)}
on:finalize={(e) => (files = e.detail.items)}
use:dndzone={{
items: files,
flipDurationMs,
zoneTabIndex: -1,
types: ['file'],
receives: ['file', 'hunk']
}}
on:consider={(e) => handleDndEvent(e, false)}
on:finalize={(e) => handleDndEvent(e, true)}
>
{#each files as file, idx (file.id)}
{#each files.filter((x) => x.hunks) as file, idx (file.id)}
<div animate:flip={{ duration: flipDurationMs }}>
<FileCard bind:file />
<FileCard bind:file on:empty={handleEmptyFile} />
</div>
{/each}
</div>

View File

@ -2,12 +2,17 @@ export type Hunk = {
id: string;
name: string;
modified: Date;
kind: string;
filePath: string;
isDndShadowItem?: boolean;
};
export type File = {
id: string;
path: string;
hunks: Hunk[];
kind: string;
isDndShadowItem?: boolean;
};
export type BranchLane = {
@ -15,4 +20,5 @@ export type BranchLane = {
name: string;
active: boolean;
files: File[];
kind: string;
};