Simplify drag & drop

- use classes for hunks, files, etc
- add class-transformer dependency
- simplify helper functions for readable code
This commit is contained in:
Mattias Granlund 2023-06-14 13:39:34 +02:00
parent 45be9746a4
commit d0c2707cae
9 changed files with 126 additions and 146 deletions

View File

@ -51,6 +51,7 @@
"@typescript-eslint/eslint-plugin": "^5.59.7",
"@typescript-eslint/parser": "^5.59.7",
"autoprefixer": "^10.4.7",
"class-transformer": "^0.5.1",
"date-fns": "^2.29.3",
"diff-match-patch": "^1.0.5",
"eslint": "^8.41.0",
@ -70,9 +71,10 @@
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.8.1",
"prettier-plugin-tailwindcss": "^0.3.0",
"reflect-metadata": "^0.1.13",
"svelte": "~3.55.1",
"svelte-check": "^3.0.1",
"svelte-dnd-action": "github:gitbutlerapp/svelte-dnd-action#fbe6a192bc99dd69d2d1946fefb2df9cea7b2de9",
"svelte-dnd-action": "github:gitbutlerapp/svelte-dnd-action#8acea8787f2172dd89ff0118bbb40bfcd5e7bf00",
"svelte-floating-ui": "^1.5.2",
"svelte-french-toast": "^1.0.3",
"svelte-loadable-store": "^1.2.3",

View File

@ -106,6 +106,9 @@ devDependencies:
autoprefixer:
specifier: ^10.4.7
version: 10.4.13(postcss@8.4.21)
class-transformer:
specifier: ^0.5.1
version: 0.5.1
date-fns:
specifier: ^2.29.3
version: 2.29.3
@ -163,6 +166,9 @@ devDependencies:
prettier-plugin-tailwindcss:
specifier: ^0.3.0
version: 0.3.0(prettier-plugin-svelte@2.9.0)(prettier@2.8.4)
reflect-metadata:
specifier: ^0.1.13
version: 0.1.13
svelte:
specifier: ~3.55.1
version: 3.55.1
@ -170,8 +176,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: github:gitbutlerapp/svelte-dnd-action#fbe6a192bc99dd69d2d1946fefb2df9cea7b2de9
version: github.com/gitbutlerapp/svelte-dnd-action/fbe6a192bc99dd69d2d1946fefb2df9cea7b2de9(svelte@3.55.1)
specifier: github:gitbutlerapp/svelte-dnd-action#8acea8787f2172dd89ff0118bbb40bfcd5e7bf00
version: github.com/gitbutlerapp/svelte-dnd-action/8acea8787f2172dd89ff0118bbb40bfcd5e7bf00(svelte@3.55.1)
svelte-floating-ui:
specifier: ^1.5.2
version: 1.5.2
@ -192,10 +198,10 @@ devDependencies:
version: 3.2.4(postcss@8.4.21)
tauri-plugin-log-api:
specifier: github:tauri-apps/tauri-plugin-log
version: github.com/tauri-apps/tauri-plugin-log/5e14c2cad7335a4284a6caad81d8cf37dd675a27
version: github.com/tauri-apps/tauri-plugin-log/21921031d74f871180381317a338559f588ad8e9
tauri-plugin-websocket-api:
specifier: github:tauri-apps/tauri-plugin-websocket
version: github.com/tauri-apps/tauri-plugin-websocket/c77fa23243388bdeff6ca1b09f78b71b62ef01da
version: github.com/tauri-apps/tauri-plugin-websocket/9633f601a828600bcf11208daaf2003b38350eaa
tinykeys:
specifier: ^1.4.0
version: 1.4.0
@ -1707,6 +1713,10 @@ packages:
fsevents: 2.3.2
dev: true
/class-transformer@0.5.1:
resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==}
dev: true
/color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@ -3405,6 +3415,10 @@ packages:
picomatch: 2.3.1
dev: true
/reflect-metadata@0.1.13:
resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==}
dev: true
/requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
dev: true
@ -4266,9 +4280,9 @@ 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
github.com/gitbutlerapp/svelte-dnd-action/8acea8787f2172dd89ff0118bbb40bfcd5e7bf00(svelte@3.55.1):
resolution: {tarball: https://codeload.github.com/gitbutlerapp/svelte-dnd-action/tar.gz/8acea8787f2172dd89ff0118bbb40bfcd5e7bf00}
id: github.com/gitbutlerapp/svelte-dnd-action/8acea8787f2172dd89ff0118bbb40bfcd5e7bf00
name: svelte-dnd-action
version: 0.9.22
peerDependencies:
@ -4277,16 +4291,16 @@ packages:
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}
github.com/tauri-apps/tauri-plugin-log/21921031d74f871180381317a338559f588ad8e9:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/21921031d74f871180381317a338559f588ad8e9}
name: tauri-plugin-log-api
version: 0.0.0
dependencies:
'@tauri-apps/api': 1.3.0
dev: true
github.com/tauri-apps/tauri-plugin-websocket/c77fa23243388bdeff6ca1b09f78b71b62ef01da:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-websocket/tar.gz/c77fa23243388bdeff6ca1b09f78b71b62ef01da}
github.com/tauri-apps/tauri-plugin-websocket/9633f601a828600bcf11208daaf2003b38350eaa:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-websocket/tar.gz/9633f601a828600bcf11208daaf2003b38350eaa}
name: tauri-plugin-websocket-api
version: 0.0.0
dependencies:

View File

@ -1,5 +1,6 @@
import type { PageLoad } from './$types';
import type { Branch, File } from './types';
import { plainToInstance } from 'class-transformer';
import { Branch, File } from './types';
export const load: PageLoad = async () => {
const testdata_file = await (
@ -26,16 +27,19 @@ export const load: PageLoad = async () => {
);
let branches = test_branches as Branch[];
branches = branches.map((column) => ({
...column,
commits: column.commits.map((commit) => ({
...commit,
files: commit.files.map((file) => ({
...file,
hunks: file.hunks.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime())
branches = plainToInstance(
Branch,
branches.map((column) => ({
...column,
commits: column.commits.map((commit) => ({
...commit,
files: commit.files.map((file) => ({
...file,
hunks: file.hunks.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime())
}))
}))
}))
}));
);
return {
branchData: branches

View File

@ -2,7 +2,7 @@
import { flip } from 'svelte/animate';
import { dndzone } from 'svelte-dnd-action';
import Lane from './BranchLane.svelte';
import type { Branch, Commit, File, Hunk } from './types';
import { Branch, Commit, File, Hunk } from './types';
import type { DndEvent } from 'svelte-dnd-action/typings';
import { createBranch, createCommit, createFile } from './helpers';
@ -10,49 +10,25 @@
const flipDurationMs = 300;
function handleDndEvent(
e: CustomEvent<DndEvent<Branch | Commit | File | Hunk>>,
isFinal: boolean
) {
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[];
function handleDndEvent(e: CustomEvent<DndEvent<Branch | Commit | File | Hunk>>) {
const newItems = e.detail.items;
const branchItems = newItems.filter((item) => item instanceof Branch) as Branch[];
const hunkItems = newItems.filter((item) => item instanceof Hunk) as Hunk[];
for (const hunk of hunkItems) {
branchItems.push(
createBranch({
commits: [
createCommit({
files: [
createFile({
hunks: [{ ...hunk, isDndShadowItem: !isFinal }],
isShadow: false,
filePath: hunk.filePath
})
],
isShadow: false
})
]
})
);
branchItems.push(createBranch(createCommit(createFile(hunk.filePath, hunk))));
}
const fileItems = newItems.filter((item) => item instanceof File) as File[];
for (const file of fileItems) {
branchItems.push(
createBranch({
commits: [
createCommit({ files: [{ ...file, isDndShadowItem: !isFinal }], isShadow: false })
]
})
);
branchItems.push(createBranch(createCommit(file)));
}
const commitItems = newItems.filter((item) => item instanceof Commit) as Commit[];
for (const commit of commitItems) {
branchItems.push(
createBranch({
commits: [commit]
})
);
branchItems.push(createBranch(commit));
}
branches = branchItems.filter((commit) => commit.active);
console.log(branches);
}
@ -73,10 +49,10 @@
types: ['branch'],
receives: ['branch', 'commit', 'file', 'hunk']
}}
on:consider={(e) => handleDndEvent(e, false)}
on:finalize={(e) => handleDndEvent(e, true)}
on:consider={handleDndEvent}
on:finalize={handleDndEvent}
>
{#each branches.filter((c) => c.active) as { id, name, commits }, idx (id)}
{#each branches.filter((c) => c.active) as { id, name, commits } (id)}
<div
class="flex h-full w-96 border border-zinc-700 bg-zinc-900/50 p-4"
animate:flip={{ duration: flipDurationMs }}

View File

@ -2,7 +2,7 @@
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 { Commit, File, Hunk } from './types';
import CommitGroup from './CommitGroup.svelte';
import { createEventDispatcher } from 'svelte';
import { createCommit, createFile } from './helpers';
@ -13,31 +13,20 @@
const flipDurationMs = 150;
const dispatch = createEventDispatcher();
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[];
function handleDndEvent(e: CustomEvent<DndEvent<Commit | File | Hunk>>) {
const newItems = e.detail.items;
const commitItems = newItems.filter((item) => item instanceof Commit) as Commit[];
// Merge hunks into existing files, or create new where none exist
const hunkItems = newItems.filter((item) => item instanceof Hunk) as Hunk[];
for (const hunk of hunkItems) {
commitItems.push(
createCommit({
files: [
createFile({
hunks: [{ ...hunk, isDndShadowItem: !isFinal }],
isShadow: false,
filePath: hunk.filePath
})
],
isShadow: false
})
);
commitItems.push(createCommit(createFile(hunk.filePath, hunk)));
}
const fileItems = newItems.filter((item) => item instanceof File) as File[];
for (const file of fileItems) {
commitItems.push(
createCommit({ files: [{ ...file, isDndShadowItem: true }], isShadow: false })
);
commitItems.push(createCommit(file));
}
commits = commitItems.filter((commit) => commit.files && commit.files.length > 0);
if (e.type == 'finalize' && (!commits || commits.length == 0)) {
@ -69,8 +58,8 @@
types: ['commit'],
receives: ['commit', 'file', 'hunk']
}}
on:consider={(e) => handleDndEvent(e, false)}
on:finalize={(e) => handleDndEvent(e, true)}
on:consider={handleDndEvent}
on:finalize={handleDndEvent}
>
{#each commits.filter((x) => x.files) as { id, description, files }, idx (id)}
<div class="w-full" animate:flip={{ duration: flipDurationMs }}>

View File

@ -2,37 +2,32 @@
import { flip } from 'svelte/animate';
import { dndzone } from 'svelte-dnd-action';
import type { DndEvent } from 'svelte-dnd-action/typings';
import type { File, Hunk } from './types';
import { File, Hunk } from './types';
import FileCard from './FileCard.svelte';
import { createEventDispatcher } from 'svelte';
import { createFile } from './helpers';
export let description: string;
export let description: string | undefined;
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[];
const hunkItems = e.detail.items.filter((item) => item.kind == 'hunk') as Hunk[];
function handleDndEvent(e: CustomEvent<DndEvent<File | Hunk>>) {
const newItems = e.detail.items;
const fileItems = newItems.filter((item) => item instanceof File) as File[];
// Merge hunks into existing files, or create new where none exist
const hunkItems = newItems.filter((item) => item instanceof Hunk) as Hunk[];
for (const hunk of hunkItems) {
const file = fileItems.find((file) => file.path == hunk.filePath);
if (file) {
file.hunks.push(hunk);
} else {
fileItems.push(
createFile({
filePath: hunk.filePath,
hunks: [{ ...hunk, isDndShadowItem: !isFinal }],
isShadow: false
})
);
fileItems.push(createFile(hunk.filePath, hunk));
}
}
files = fileItems.filter((file) => file.hunks && file.hunks.length > 0);
if (e.type == 'finalize' && (!files || files.length == 0)) {
@ -63,8 +58,8 @@
types: ['file'],
receives: ['file', 'hunk']
}}
on:consider={(e) => handleDndEvent(e, false)}
on:finalize={(e) => handleDndEvent(e, true)}
on:consider={handleDndEvent}
on:finalize={handleDndEvent}
>
{#each files.filter((x) => x.hunks) as file, idx (file.id)}
<div class="w-full" animate:flip={{ duration: flipDurationMs }}>

View File

@ -1,38 +1,37 @@
import type { Branch, Commit, File, Hunk } from './types';
import { Branch, Commit, File, type Hunk } from './types';
import { plainToInstance } from 'class-transformer';
let fileCounter = 0;
let commitCounter = 0;
let branchCounter = 0;
export function createFile(args: { hunks: [Hunk]; filePath: string; isShadow: boolean }): File {
export function createFile(path: string, hunk: Hunk): File {
fileCounter++;
return {
return plainToInstance(File, {
id: `file-${fileCounter}`,
path: args.filePath,
path: path,
kind: 'file',
hunks: args.hunks,
isDndShadowItem: args.isShadow
};
hunks: [hunk]
});
}
export function createCommit(args: { files: File[]; isShadow: boolean }): Commit {
export function createCommit(file: File): Commit {
commitCounter++;
return {
return plainToInstance(Commit, {
id: `commit-${commitCounter}`,
description: `New commit # ${commitCounter}`,
kind: 'commit',
files: args.files,
isDndShadowItem: args.isShadow
};
files: [file]
});
}
export function createBranch(args: { commits: Commit[] }): Branch {
export function createBranch(commit: Commit): Branch {
branchCounter++;
return {
return plainToInstance(Branch, {
id: `branch-${branchCounter}`,
name: `new branch ${branchCounter}`,
active: true,
kind: 'branch',
commits: args.commits
};
commits: [commit]
});
}

View File

@ -1,35 +1,35 @@
export type Hunk = {
id: string;
name: string;
modifiedAt: Date;
diff: string;
kind: string;
filePath: string;
isDndShadowItem?: boolean;
};
import { Type } from 'class-transformer';
import 'reflect-metadata';
export type File = {
id: string;
path: string;
hunks: Hunk[];
kind: string;
class DndItem {
id!: string;
kind!: string;
isDndShadowItem?: boolean;
};
}
export type Commit = {
id: string;
description: string;
export class Hunk extends DndItem {
name!: string;
diff!: string;
modifiedAt!: Date;
filePath!: string;
}
export class File extends DndItem {
path!: string;
@Type(() => Hunk)
hunks!: Hunk[];
}
export class Commit extends DndItem {
description?: string;
committedAt?: Date;
files: File[];
kind: string;
isDndShadowItem?: boolean;
};
@Type(() => File)
files!: File[];
}
export type Branch = {
id: string;
name: string;
active: boolean;
commits: Commit[];
kind: string;
isDndShadowItem?: boolean;
};
export class Branch extends DndItem {
name!: string;
active!: boolean;
@Type(() => Commit)
commits!: Commit[];
}

View File

@ -8,6 +8,7 @@
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
"strict": true,
"experimentalDecorators": true
}
}