Merge pull request #4109 from gitbutlerapp/update-modal-component-branch

Update modal component branch
This commit is contained in:
Caleb Owens 2024-06-21 09:55:57 +02:00 committed by GitHub
commit ffaffe6cde
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 894 additions and 609 deletions

View File

@ -0,0 +1,53 @@
import { DraggableCommit, DraggableHunk, DraggableFile } from '$lib/dragging/draggables';
import { filesToOwnership } from '$lib/vbranches/ownership';
import type { BranchController } from '$lib/vbranches/branchController';
import type { Branch } from '$lib/vbranches/types';
class BranchDragActions {
constructor(
private branchController: BranchController,
private branch: Branch
) {}
acceptMoveCommit(data: any) {
return data instanceof DraggableCommit && data.branchId !== this.branch.id && data.isHeadCommit;
}
onMoveCommit(data: DraggableCommit) {
this.branchController.moveCommit(this.branch.id, data.commit.id);
}
acceptBranchDrop(data: any) {
if (data instanceof DraggableHunk && data.branchId !== this.branch.id) {
return !data.hunk.locked;
} else if (data instanceof DraggableFile && data.branchId && data.branchId !== this.branch.id) {
return !data.files.some((f) => f.locked);
} else {
return false;
}
}
onBranchDrop(data: DraggableHunk | DraggableFile) {
if (data instanceof DraggableHunk) {
const newOwnership = `${data.hunk.filePath}:${data.hunk.id}`;
this.branchController.updateBranchOwnership(
this.branch.id,
(newOwnership + '\n' + this.branch.ownership).trim()
);
} else if (data instanceof DraggableFile) {
const newOwnership = filesToOwnership(data.files);
this.branchController.updateBranchOwnership(
this.branch.id,
(newOwnership + '\n' + this.branch.ownership).trim()
);
}
}
}
export class BranchDragActionsFactory {
constructor(private branchController: BranchController) {}
build(branch: Branch) {
return new BranchDragActions(this.branchController, branch);
}
}

View File

@ -0,0 +1,108 @@
import { DraggableCommit, DraggableFile, DraggableHunk } from '$lib/dragging/draggables';
import { filesToOwnership, filesToSimpleOwnership } from '$lib/vbranches/ownership';
import {
LocalFile,
RemoteCommit,
RemoteFile,
type Branch,
type Commit
} from '$lib/vbranches/types';
import type { Project } from '$lib/backend/projects';
import type { BranchController } from '$lib/vbranches/branchController';
class CommitDragActions {
constructor(
private branchController: BranchController,
private project: Project,
private branch: Branch,
private commit: Commit | RemoteCommit
) {}
acceptAmend(data: any) {
if (this.commit instanceof RemoteCommit) {
return false;
}
if (!this.project.ok_with_force_push && this.commit.isRemote) {
return false;
}
if (this.commit.isIntegrated) {
return false;
}
if (data instanceof DraggableHunk && data.branchId === this.branch.id) {
return true;
} else if (data instanceof DraggableFile && data.branchId === this.branch.id) {
return true;
} else {
return false;
}
}
onAmend(data: any) {
if (data instanceof DraggableHunk) {
const newOwnership = `${data.hunk.filePath}:${data.hunk.id}`;
this.branchController.amendBranch(this.branch.id, this.commit.id, newOwnership);
} else if (data instanceof DraggableFile) {
if (data.file instanceof LocalFile) {
// this is an uncommitted file change being amended to a previous commit
const newOwnership = filesToOwnership(data.files);
this.branchController.amendBranch(this.branch.id, this.commit.id, newOwnership);
} else if (data.file instanceof RemoteFile) {
// this is a file from a commit, rather than an uncommitted file
const newOwnership = filesToSimpleOwnership(data.files);
if (data.commit) {
this.branchController.moveCommitFile(
this.branch.id,
data.commit.id,
this.commit.id,
newOwnership
);
}
}
}
}
acceptSquash(data: any) {
if (this.commit instanceof RemoteCommit) {
return false;
}
if (!(data instanceof DraggableCommit)) return false;
if (data.branchId !== this.branch.id) return false;
if (data.commit.isParentOf(this.commit)) {
if (data.commit.isIntegrated) return false;
if (data.commit.isRemote && !this.project.ok_with_force_push) return false;
return true;
} else if (this.commit.isParentOf(data.commit)) {
if (this.commit.isIntegrated) return false;
if (this.commit.isRemote && !this.project.ok_with_force_push) return false;
return true;
} else {
return false;
}
}
onSquash(data: any) {
if (this.commit instanceof RemoteCommit) {
return;
}
if (data.commit.isParentOf(this.commit)) {
this.branchController.squashBranchCommit(data.branchId, this.commit.id);
} else if (this.commit.isParentOf(data.commit)) {
this.branchController.squashBranchCommit(data.branchId, data.commit.id);
}
}
}
export class CommitDragActionsFactory {
constructor(
private branchController: BranchController,
private project: Project
) {}
build(branch: Branch, commit: Commit | RemoteCommit) {
return new CommitDragActions(this.branchController, this.project, branch, commit);
}
}

View File

@ -1,8 +1,8 @@
<script lang="ts" async="true">
import FullviewLoading from './FullviewLoading.svelte';
import NewBranchDropZone from './NewBranchDropZone.svelte';
import dzenSvg from '$lib/assets/dzen-pc.svg?raw';
import { Project } from '$lib/backend/projects';
import BranchDropzone from '$lib/components/BranchDropzone.svelte';
import BranchLane from '$lib/components/BranchLane.svelte';
import Icon from '$lib/components/Icon.svelte';
import { cloneWithRotation } from '$lib/dragging/draggable';
@ -193,7 +193,7 @@
</div>
</div>
{:else}
<NewBranchDropZone />
<BranchDropzone />
{/if}
</div>
{/if}

View File

@ -4,7 +4,6 @@
import BranchHeader from './BranchHeader.svelte';
import CommitDialog from './CommitDialog.svelte';
import CommitList from './CommitList.svelte';
import DropzoneOverlay from './DropzoneOverlay.svelte';
import EmptyStatePlaceholder from './EmptyStatePlaceholder.svelte';
import InfoMessage from './InfoMessage.svelte';
import PullRequestCard from './PullRequestCard.svelte';
@ -14,16 +13,10 @@
import laneNewSvg from '$lib/assets/empty-state/lane-new.svg?raw';
import noChangesSvg from '$lib/assets/empty-state/lane-no-changes.svg?raw';
import { Project } from '$lib/backend/projects';
import BranchCardDropzones from '$lib/components/BranchCard/Dropzones.svelte';
import Resizer from '$lib/components/Resizer.svelte';
import { projectAiGenAutoBranchNamingEnabled } from '$lib/config/config';
import { projectAiGenEnabled } from '$lib/config/config';
import {
DraggableCommit,
DraggableFile,
DraggableHunk,
DraggableRemoteCommit
} from '$lib/dragging/draggables';
import { dropzone } from '$lib/dragging/dropzone';
import { showError } from '$lib/notifications/toasts';
import { persisted } from '$lib/persisted/persisted';
import { SETTINGS, type Settings } from '$lib/settings/userSettings';
@ -32,7 +25,6 @@
import { computeAddedRemovedByFiles } from '$lib/utils/metrics';
import { BranchController } from '$lib/vbranches/branchController';
import { FileIdSelection } from '$lib/vbranches/fileIdSelection';
import { filesToOwnership } from '$lib/vbranches/ownership';
import { Branch } from '$lib/vbranches/types';
import lscache from 'lscache';
import { onMount } from 'svelte';
@ -106,47 +98,6 @@
onMount(() => {
laneWidth = lscache.get(laneWidthKey + branch.id);
});
function acceptMoveCommit(data: any) {
return data instanceof DraggableCommit && data.branchId !== branch.id && data.isHeadCommit;
}
function onCommitDrop(data: DraggableCommit) {
branchController.moveCommit(branch.id, data.commit.id);
}
function acceptCherrypick(data: any) {
return data instanceof DraggableRemoteCommit && data.branchId === branch.id;
}
function onCherrypicked(data: DraggableRemoteCommit) {
branchController.cherryPick(branch.id, data.remoteCommit.id);
}
function acceptBranchDrop(data: any) {
if (data instanceof DraggableHunk && data.branchId !== branch.id) {
return !data.hunk.locked;
} else if (data instanceof DraggableFile && data.branchId && data.branchId !== branch.id) {
return !data.files.some((f) => f.locked);
} else {
return false;
}
}
function onBranchDrop(data: DraggableHunk | DraggableFile) {
if (data instanceof DraggableHunk) {
const newOwnership = `${data.hunk.filePath}:${data.hunk.id}`;
branchController.updateBranchOwnership(
branch.id,
(newOwnership + '\n' + branch.ownership).trim()
);
} else if (data instanceof DraggableFile) {
const newOwnership = filesToOwnership(data.files);
branchController.updateBranchOwnership(
branch.id,
(newOwnership + '\n' + branch.ownership).trim()
);
}
}
</script>
{#if $isLaneCollapsed}
@ -194,40 +145,9 @@
}}
/>
<PullRequestCard />
<!-- DROPZONES -->
<DropzoneOverlay class="cherrypick-dz-marker" label="Apply here" />
<DropzoneOverlay class="cherrypick-dz-marker" label="Apply here" />
<DropzoneOverlay class="lane-dz-marker" label="Move here" />
<div class="card">
<div
class="branch-card__dropzone-wrapper"
use:dropzone={{
hover: 'move-commit-dz-hover',
active: 'move-commit-dz-active',
accepts: acceptMoveCommit,
onDrop: onCommitDrop,
disabled: isUnapplied
}}
use:dropzone={{
hover: 'cherrypick-dz-hover',
active: 'cherrypick-dz-active',
accepts: acceptCherrypick,
onDrop: onCherrypicked,
disabled: isUnapplied
}}
use:dropzone={{
hover: 'lane-dz-hover',
active: 'lane-dz-active',
accepts: acceptBranchDrop,
onDrop: onBranchDrop,
disabled: isUnapplied
}}
>
<DropzoneOverlay class="cherrypick-dz-marker" label="Apply here" />
<DropzoneOverlay class="lane-dz-marker" label="Move here" />
<DropzoneOverlay class="move-commit-dz-marker" label="Move here" />
<BranchCardDropzones>
{#if branch.files?.length > 0}
<div class="branch-card__files">
<BranchFiles
@ -282,7 +202,7 @@
</EmptyStatePlaceholder>
</div>
{/if}
</div>
</BranchCardDropzones>
<div class="card-commits">
<CommitList {isUnapplied} />
@ -331,13 +251,6 @@
height: 100%;
}
.branch-card__dropzone-wrapper {
display: flex;
flex-direction: column;
flex: 1;
position: relative;
}
.branch-card__contents {
position: relative;
display: flex;
@ -358,6 +271,7 @@
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
/* border-left: 1px solid var(--clr-border-2);
border-right: 1px solid var(--clr-border-2);
border-radius: var(--radius-m) var(--radius-m) 0 0; */
@ -373,7 +287,7 @@
.no-changes {
user-select: none;
display: flex;
flex-grow: 1;
height: 100%;
flex-direction: column;
align-items: center;
color: var(--clr-scale-ntrl-60);
@ -383,19 +297,6 @@
cursor: default; /* was defaulting to text cursor */
}
/* hunks drop zone */
/* cherry pick drop zone */
/* move commit drop zone */
/* squash drop zone */
:global(
.lane-dz-active .lane-dz-marker,
.cherrypick-dz-active .cherrypick-dz-marker,
.move-commit-dz-active .move-commit-dz-marker,
.squash-dz-active .squash-dz-marker
) {
display: flex;
}
.branch-card :global(.contents) {
display: flex;
flex-direction: column;

View File

@ -0,0 +1,72 @@
<script lang="ts">
import { BranchDragActionsFactory } from '$lib/branches/dragActions';
import CardOverlay from '$lib/components/Dropzone/CardOverlay.svelte';
import Dropzone from '$lib/components/Dropzone/Dropzone.svelte';
import { getContext, getContextStore } from '$lib/utils/context';
import { Branch } from '$lib/vbranches/types';
import type { Snippet } from 'svelte';
const branchDragActionsFactory = getContext(BranchDragActionsFactory);
const branch = getContextStore(Branch);
interface Props {
children: Snippet;
}
const { children }: Props = $props();
const actions = $derived(branchDragActionsFactory.build($branch));
</script>
<div class="commit-list-item">
<div class="commit-card-wrapper">
{@render moveCommitDropzone()}
</div>
</div>
<!-- We require the dropzones to be nested -->
{#snippet moveCommitDropzone()}
<Dropzone
accepts={actions.acceptMoveCommit.bind(actions)}
ondrop={actions.onMoveCommit.bind(actions)}
fillHeight
>
{@render branchDropDropzone()}
{#snippet overlay({ hovered, activated })}
<CardOverlay {hovered} {activated} label="Move here" />
{/snippet}
</Dropzone>
{/snippet}
{#snippet branchDropDropzone()}
<Dropzone
accepts={actions.acceptBranchDrop.bind(actions)}
ondrop={actions.onBranchDrop.bind(actions)}
fillHeight
>
{@render children()}
{#snippet overlay({ hovered, activated })}
<CardOverlay {hovered} {activated} label="Move here" />
{/snippet}
</Dropzone>
{/snippet}
<style>
.commit-list-item {
display: flex;
position: relative;
padding: 0;
gap: 8px;
flex-grow: 1;
overflow: hidden;
&:last-child {
padding-bottom: 0;
}
}
.commit-card-wrapper {
position: relative;
width: 100%;
}
</style>

View File

@ -6,8 +6,8 @@
import topSheetSvg from '$lib/assets/new-branch/top-sheet.svg?raw';
// import components
import Button from '$lib/components/Button.svelte';
import Dropzone from '$lib/components/Dropzone/Dropzone.svelte';
import { DraggableFile, DraggableHunk } from '$lib/dragging/draggables';
import { dropzone } from '$lib/dragging/dropzone';
import { getContext } from '$lib/utils/context';
import { BranchController } from '$lib/vbranches/branchController';
import { filesToOwnership } from '$lib/vbranches/ownership';
@ -31,16 +31,10 @@
}
</script>
<div
class="canvas-dropzone"
use:dropzone={{
active: 'new-dz-active',
hover: 'new-dz-hover',
onDrop,
accepts
}}
>
<div id="new-branch-dz" class="new-virtual-branch">
<div class="canvas-dropzone">
<Dropzone {accepts} ondrop={onDrop}>
{#snippet overlay({ hovered, activated })}
<div class="new-virtual-branch" class:activated class:hovered>
<div class="new-virtual-branch__content">
<div class="stimg">
<div class="stimg__hand">
@ -74,6 +68,8 @@
>
</div>
</div>
{/snippet}
</Dropzone>
</div>
<style lang="postcss">
@ -90,6 +86,7 @@
align-items: center;
justify-content: center;
width: 352px;
height: 100%;
border-radius: var(--radius-m);
border: 1px dashed var(--clr-border-2);
background-color: transparent;
@ -214,7 +211,7 @@
}
/* DRAGZONE MODIEFIERS */
:global(.canvas-dropzone.new-dz-active) {
.activated {
&.new-virtual-branch {
background-color: oklch(from var(--clr-scale-pop-70) l c h / 0.1);
border: 1px dashed oklch(from var(--clr-scale-pop-40) l c h / 0.8);

View File

@ -159,7 +159,7 @@
kind="solid"
on:click={async () => {
await branchController.deleteBranch(branch.id);
deleteBranchModal.close();
close();
}}
>
Delete

View File

@ -1,130 +1,59 @@
<script lang="ts">
import DropzoneOverlay from './DropzoneOverlay.svelte';
import { Project } from '$lib/backend/projects';
import { DraggableCommit, DraggableFile, DraggableHunk } from '$lib/dragging/draggables';
import { dropzone } from '$lib/dragging/dropzone';
import { CommitDragActionsFactory } from '$lib/commits/dragActions';
import CardOverlay from '$lib/components/Dropzone/CardOverlay.svelte';
import Dropzone from '$lib/components/Dropzone/Dropzone.svelte';
import { getContext, maybeGetContextStore } from '$lib/utils/context';
import { BranchController } from '$lib/vbranches/branchController';
import { filesToOwnership, filesToSimpleOwnership } from '$lib/vbranches/ownership';
import { RemoteCommit, Branch, Commit, LocalFile, RemoteFile } from '$lib/vbranches/types';
import { RemoteCommit, Branch, Commit } from '$lib/vbranches/types';
import type { Snippet } from 'svelte';
export let commit: Commit | RemoteCommit;
const commitDragActionsFactory = getContext(CommitDragActionsFactory);
interface Props {
commit: Commit | RemoteCommit;
children: Snippet;
}
const { commit, children }: Props = $props();
const branchController = getContext(BranchController);
const project = getContext(Project);
const branch = maybeGetContextStore(Branch);
function acceptAmend(commit: Commit | RemoteCommit) {
const actions = $derived.by(() => {
if (!$branch) return;
if (commit instanceof RemoteCommit) {
return () => false;
}
return (data: any) => {
if (!project.ok_with_force_push && commit.isRemote) {
return false;
}
if (commit.isIntegrated) {
return false;
}
if (data instanceof DraggableHunk && data.branchId === $branch.id) {
return true;
} else if (data instanceof DraggableFile && data.branchId === $branch.id) {
return true;
} else {
return false;
}
};
}
function onAmend(commit: Commit | RemoteCommit) {
if (!$branch) return;
return (data: any) => {
if (data instanceof DraggableHunk) {
const newOwnership = `${data.hunk.filePath}:${data.hunk.id}`;
branchController.amendBranch($branch.id, commit.id, newOwnership);
} else if (data instanceof DraggableFile) {
if (data.file instanceof LocalFile) {
// this is an uncommitted file change being amended to a previous commit
const newOwnership = filesToOwnership(data.files);
branchController.amendBranch($branch.id, commit.id, newOwnership);
} else if (data.file instanceof RemoteFile) {
// this is a file from a commit, rather than an uncommitted file
const newOwnership = filesToSimpleOwnership(data.files);
if (data.commit) {
branchController.moveCommitFile($branch.id, data.commit.id, commit.id, newOwnership);
}
}
}
};
}
function acceptSquash(commit: Commit | RemoteCommit) {
if (!$branch) return;
if (commit instanceof RemoteCommit) {
return () => false;
}
return (data: any) => {
if (!(data instanceof DraggableCommit)) return false;
if (data.branchId !== $branch.id) return false;
if (data.commit.isParentOf(commit)) {
if (data.commit.isIntegrated) return false;
if (data.commit.isRemote && !project.ok_with_force_push) return false;
return true;
} else if (commit.isParentOf(data.commit)) {
if (commit.isIntegrated) return false;
if (commit.isRemote && !project.ok_with_force_push) return false;
return true;
} else {
return false;
}
};
}
function onSquash(commit: Commit | RemoteCommit) {
if (!$branch) return;
if (commit instanceof RemoteCommit) {
return () => false;
}
return (data: DraggableCommit) => {
if (data.commit.isParentOf(commit)) {
branchController.squashBranchCommit(data.branchId, commit.id);
} else if (commit.isParentOf(data.commit)) {
branchController.squashBranchCommit(data.branchId, data.commit.id);
}
};
}
return commitDragActionsFactory.build($branch, commit);
});
</script>
<div class="commit-list-item">
<div
class="commit-card-wrapper"
use:dropzone={{
active: 'amend-dz-active',
hover: 'amend-dz-hover',
accepts: acceptAmend(commit),
onDrop: onAmend(commit)
}}
use:dropzone={{
active: 'squash-dz-active',
hover: 'squash-dz-hover',
accepts: acceptSquash(commit),
onDrop: onSquash(commit)
}}
>
<!-- DROPZONES -->
<DropzoneOverlay class="amend-dz-marker" label="Amend" />
<DropzoneOverlay class="squash-dz-marker" label="Squash" />
<div class="commit-card-wrapper">
{#if actions}
{@render ammendDropzone()}
{:else}
{@render children()}
{/if}
</div>
</div>
<slot />
</div>
</div>
<!-- We require the dropzones to be nested -->
{#snippet ammendDropzone()}
<Dropzone accepts={actions!.acceptAmend.bind(actions)} ondrop={actions!.onAmend.bind(actions)}>
{@render squashDropzone()}
{#snippet overlay({ hovered, activated })}
<CardOverlay {hovered} {activated} label="Ammend commit" />
{/snippet}
</Dropzone>
{/snippet}
{#snippet squashDropzone()}
<Dropzone accepts={actions!.acceptSquash.bind(actions)} ondrop={actions!.onSquash.bind(actions)}>
{@render children()}
{#snippet overlay({ hovered, activated })}
<CardOverlay {hovered} {activated} label="Squash commit" />
{/snippet}
</Dropzone>
{/snippet}
<style>
.commit-list-item {

View File

@ -2,9 +2,13 @@
import CommitCard from './CommitCard.svelte';
import CommitLines from './CommitLines.svelte';
import { Project } from '$lib/backend/projects';
import ReorderDropzone from '$lib/components/CommitList/ReorderDropzone.svelte';
import Dropzone from '$lib/components/Dropzone/Dropzone.svelte';
import LineOverlay from '$lib/components/Dropzone/LineOverlay.svelte';
import InsertEmptyCommitAction from '$lib/components/InsertEmptyCommitAction.svelte';
import { ReorderDropzoneIndexer } from '$lib/dragging/reorderDropzoneIndexer';
import {
ReorderDropzoneManagerFactory,
type ReorderDropzone
} from '$lib/dragging/reorderDropzoneManager';
import { getAvatarTooltip } from '$lib/utils/avatar';
import { getContext } from '$lib/utils/context';
import { getContextStore } from '$lib/utils/context';
@ -29,6 +33,8 @@
const project = getContext(Project);
const branchController = getContext(BranchController);
const reorderDropzoneManagerFactory = getContext(ReorderDropzoneManagerFactory);
// Force the "base" commit lines to update when $branch updates.
let tsKey: number | undefined;
$: {
@ -44,7 +50,10 @@
$: hasIntegratedCommits = $integratedCommits.length > 0;
$: hasRemoteCommits = $remoteCommits.length > 0;
$: hasShadowedCommits = $localCommits.some((c) => c.relatedTo);
$: reorderDropzoneIndexer = new ReorderDropzoneIndexer([...$localCommits, ...$remoteCommits]);
$: reorderDropzoneManager = reorderDropzoneManagerFactory.build($branch, [
...$localCommits,
...$remoteCommits
]);
$: forkPoint = $branch.forkPoint;
$: upstreamForkPoint = $branch.upstreamData?.forkPoint;
@ -97,8 +106,31 @@
}
branchController.insertBlankCommit($branch.id, commitId, location === 'above' ? -1 : 1);
}
function getReorderDropzoneOffset({
isFirst = false,
isMiddle = false,
isLast = false
}: {
isFirst?: boolean;
isMiddle?: boolean;
isLast?: boolean;
}) {
if (isFirst) return 12;
if (isMiddle) return 6;
if (isLast) return 0;
return 0;
}
</script>
{#snippet reorderDropzone(dropzone: ReorderDropzone, yOffsetPx: number)}
<Dropzone accepts={dropzone.accepts.bind(dropzone)} ondrop={dropzone.onDrop.bind(dropzone)}>
{#snippet overlay({ hovered, activated })}
<LineOverlay {hovered} {activated} {yOffsetPx} />
{/snippet}
</Dropzone>
{/snippet}
{#if hasCommits || hasUnknownCommits}
<div class="commits">
<!-- UPSTREAM COMMITS -->
@ -138,10 +170,10 @@
<InsertEmptyCommitAction isFirst on:click={() => insertBlankCommit($branch.head, 'above')} />
<!-- LOCAL COMMITS -->
{#if $localCommits.length > 0}
<ReorderDropzone
index={reorderDropzoneIndexer.topDropzoneIndex}
indexer={reorderDropzoneIndexer}
/>
{@render reorderDropzone(
reorderDropzoneManager.topDropzone,
getReorderDropzoneOffset({ isFirst: true })
)}
{#each $localCommits as commit, idx (commit.id)}
<CommitCard
{commit}
@ -176,10 +208,14 @@
</svelte:fragment>
</CommitCard>
<ReorderDropzone
index={reorderDropzoneIndexer.dropzoneIndexBelowCommit(commit.id)}
indexer={reorderDropzoneIndexer}
/>
{@render reorderDropzone(
reorderDropzoneManager.dropzoneBelowCommit(commit.id),
getReorderDropzoneOffset({
isLast: $remoteCommits.length === 0 && idx + 1 === $localCommits.length,
isMiddle: $remoteCommits.length > 0 && idx + 1 === $localCommits.length
})
)}
<InsertEmptyCommitAction
isLast={$remoteCommits.length === 0 && idx + 1 === $localCommits.length}
isMiddle={$remoteCommits.length > 0 && idx + 1 === $localCommits.length}
@ -219,10 +255,12 @@
/>
</svelte:fragment>
</CommitCard>
<ReorderDropzone
index={reorderDropzoneIndexer.dropzoneIndexBelowCommit(commit.id)}
indexer={reorderDropzoneIndexer}
/>
{@render reorderDropzone(
reorderDropzoneManager.dropzoneBelowCommit(commit.id),
getReorderDropzoneOffset({
isLast: idx + 1 === $remoteCommits.length
})
)}
<InsertEmptyCommitAction
isLast={idx + 1 === $remoteCommits.length}
on:click={() => insertBlankCommit(commit.id, 'below')}

View File

@ -1,56 +0,0 @@
<script lang="ts">
import DropzoneOverlay from '$lib/components/DropzoneOverlay.svelte';
import { DraggableCommit } from '$lib/dragging/draggables';
import { dropzone } from '$lib/dragging/dropzone';
import { getContext, getContextStore } from '$lib/utils/context';
import { BranchController } from '$lib/vbranches/branchController';
import { Branch } from '$lib/vbranches/types';
import type { ReorderDropzoneIndexer } from '$lib/dragging/reorderDropzoneIndexer';
export let index: number;
export let indexer: ReorderDropzoneIndexer;
const branchController = getContext(BranchController);
const branch = getContextStore(Branch);
function accepts(data: any) {
if (!(data instanceof DraggableCommit)) return false;
if (data.branchId !== $branch.id) return false;
if (indexer.dropzoneCommitOffset(index, data.commit.id) === 0) return false;
return true;
}
function onDrop(data: any) {
if (!(data instanceof DraggableCommit)) return;
if (data.branchId !== $branch.id) return;
const offset = indexer.dropzoneCommitOffset(index, data.commit.id);
branchController.reorderCommit($branch.id, data.commit.id, offset);
}
</script>
<div
class="dropzone"
use:dropzone={{ accepts, onDrop, active: 'reorder-dz-active', hover: 'reorder-dz-hover' }}
>
<DropzoneOverlay class="reorder-dz-marker" label="Reorder" />
</div>
<style lang="postcss">
:root {
/* There is something that is increasing the width by 26 pixels and I'm not quite sure what it is */
--dropzone-width: calc(100% - 26px);
}
:global(.reorder-dz-active .reorder-dz-marker) {
display: flex !important;
height: 48px;
width: var(--dropzone-width);
}
:global(.reorder-dz-active) {
height: 48px;
width: var(--dropzone-width);
}
</style>

View File

@ -0,0 +1,68 @@
<script lang="ts">
import Icon from '$lib/components/Icon.svelte';
interface Props {
hovered: boolean;
activated: boolean;
label?: string;
}
const { hovered, activated, label = 'Drop here' }: Props = $props();
</script>
<div class="dropzone-target container" class:activated class:hovered>
<div class="dropzone-content">
<Icon name="new-file-small-filled" />
<p class="text-base-13">{label}</p>
</div>
</div>
<style lang="postcss">
:root {
--dropzone-height: 16px;
--dropzone-overlap: calc(var(--dropzone-height) / 2);
}
.container {
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: oklch(from var(--clr-scale-pop-70) l c h / 0.1);
outline-color: var(--clr-scale-pop-40);
outline-style: dashed;
outline-width: 1px;
outline-offset: -10px;
backdrop-filter: blur(10px);
z-index: var(--z-lifted);
transition: background-color 0.1s;
/* It is very important that all children are pointer-events: none */
/* https://stackoverflow.com/questions/7110353/html5-dragleave-fired-when-hovering-a-child-element */
& * {
pointer-events: none;
}
&:not(.activated) {
display: none;
}
&.hovered {
background-color: oklch(from var(--clr-scale-pop-20) l c h / 0.1);
}
}
.dropzone-content {
display: flex;
align-items: center;
gap: 6px;
color: var(--clr-scale-pop-40);
}
</style>

View File

@ -0,0 +1,68 @@
<script lang="ts">
import { dropzone } from '$lib/dragging/dropzone';
import type { Snippet } from 'svelte';
interface Props {
disabled?: boolean;
fillHeight?: boolean;
accepts: (data: any) => boolean;
ondrop: (data: any) => Promise<void> | void;
overlay: Snippet<[{ hovered: boolean; activated: boolean }]>;
children?: Snippet;
}
const {
disabled = false,
fillHeight = false,
accepts,
ondrop,
overlay,
children
}: Props = $props();
let hovered = $state(false);
// When a draggable is being hovered over the dropzone
function onHoverStart() {
hovered = true;
}
function onHoverEnd() {
hovered = false;
}
let activated = $state(false);
// Fired when a draggable is first picked up and the dropzone accepts the draggable
function onActivationStart() {
activated = true;
}
function onActivationEnd() {
activated = false;
}
</script>
<div
use:dropzone={{
disabled,
accepts,
onDrop: ondrop,
onHoverStart,
onHoverEnd,
onActivationStart,
onActivationEnd,
target: '.dropzone-target'
}}
class:fill-height={fillHeight}
>
{@render overlay({ hovered, activated })}
{#if children}
{@render children()}
{/if}
</div>
<style lang="postcss">
.fill-height {
height: 100%;
}
</style>

View File

@ -0,0 +1,62 @@
<script lang="ts">
import { pxToRem } from '$lib/utils/pxToRem';
interface Props {
hovered: boolean;
activated: boolean;
yOffsetPx?: number;
}
const { hovered, activated, yOffsetPx = 0 }: Props = $props();
</script>
<div
class="dropzone-target container"
class:activated
style="--y-offset: {pxToRem(yOffsetPx) || 0}"
>
<div class="indicator" class:hovered></div>
</div>
<style lang="postcss">
:root {
--dropzone-height: 16px;
--dropzone-overlap: calc(var(--dropzone-height) / 2);
}
.container {
height: var(--dropzone-height);
margin-top: calc(var(--dropzone-overlap) * -1);
margin-bottom: calc(var(--dropzone-overlap) * -1);
width: 100%;
position: relative;
top: var(--y-offset);
display: flex;
align-items: center;
z-index: 101;
/* It is very important that all children are pointer-events: none */
/* https://stackoverflow.com/questions/7110353/html5-dragleave-fired-when-hovering-a-child-element */
& * {
pointer-events: none;
}
&:not(.activated) {
display: none;
}
}
.indicator {
width: 100%;
height: 3px;
transition: opacity 0.1s;
background-color: var(--clr-border-2);
opacity: 0;
&.hovered {
opacity: 1;
}
}
</style>

View File

@ -1,74 +0,0 @@
<script lang="ts">
import Icon from '$lib/components/Icon.svelte';
let className = '';
export { className as class };
export let label = 'Drop here';
export let radius: 's' | 'm' | 'l' | undefined = 'm';
</script>
<div
class="{className} dropzone"
class:small={radius === 's'}
class:medium={radius === 'm'}
class:large={radius === 'l'}
>
<div class="dropzone-wrapper">
<div class="dropzone-content">
<Icon name="new-file-small-filled" />
<span class="text-base-13">{label}</span>
</div>
</div>
</div>
<style lang="postcss">
@layer dropzone {
.dropzone {
user-select: none;
display: none;
z-index: var(--z-lifted);
position: absolute;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
background-color: oklch(from var(--clr-scale-pop-70) l c h / 0.1);
outline-color: var(--clr-scale-pop-40);
outline-style: dashed;
outline-width: 1px;
outline-offset: -10px;
backdrop-filter: blur(10px);
}
.dropzone-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.dropzone-content {
display: flex;
align-items: center;
gap: 6px;
color: var(--clr-scale-pop-40);
}
.small {
border-radius: var(--radius-s);
}
.medium {
border-radius: var(--radius-m);
}
.large {
border-radius: var(--radius-l);
}
}
</style>

View File

@ -6,21 +6,14 @@
interface Props {
width?: 'default' | 'small' | 'large';
title?: string | undefined;
icon?: keyof typeof iconsJson | undefined;
title?: string;
icon?: keyof typeof iconsJson;
onclose?: () => void;
children: Snippet<[item?: any]>;
controls?: Snippet<[close: () => void, item: any]>;
}
const {
width = 'default',
title = undefined,
icon = undefined,
onclose,
children,
controls
}: Props = $props();
const { width = 'default', title, icon, onclose, children, controls }: Props = $props();
let dialog = $state<HTMLDialogElement>();
let item = $state<any>();

View File

@ -1,4 +1,4 @@
import { dzRegistry } from './dropzone';
import { dropzoneRegistry } from './dropzone';
import type { Draggable } from './draggables';
export interface DraggableConfig {
@ -79,11 +79,6 @@ export function draggable(node: HTMLElement, initialOpts: DraggableConfig) {
let selectedElements: HTMLElement[] = [];
const onDropListeners = new Map<HTMLElement, Array<(e: DragEvent) => void>>();
const onDragLeaveListeners = new Map<HTMLElement, Array<(e: DragEvent) => void>>();
const onDragEnterListeners = new Map<HTMLElement, Array<(e: DragEvent) => void>>();
const onDragOverListeners = new Map<HTMLElement, Array<(e: DragEvent) => void>>();
function handleMouseDown(e: MouseEvent) {
dragHandle = e.target as HTMLElement;
}
@ -124,68 +119,13 @@ export function draggable(node: HTMLElement, initialOpts: DraggableConfig) {
document.body.appendChild(clone);
// activate destination zones
dzRegistry.forEach(async ([target, dz]) => {
if (!dz.accepts(await opts.data)) return;
async function onDrop(e: DragEvent) {
e.preventDefault();
dz.onDrop(await opts.data);
}
function onDragEnter(e: DragEvent) {
e.preventDefault();
target.classList.add(dz.hover);
}
function onDragLeave(e: DragEvent) {
e.preventDefault();
target.classList.remove(dz.hover);
}
function onDragOver(e: DragEvent) {
e.preventDefault();
}
// keep track of listeners so that we can remove them later
if (onDropListeners.has(target)) {
onDropListeners.get(target)!.push(onDrop);
} else {
onDropListeners.set(target, [onDrop]);
}
if (onDragEnterListeners.has(target)) {
onDragEnterListeners.get(target)!.push(onDragEnter);
} else {
onDragEnterListeners.set(target, [onDragEnter]);
}
if (onDragLeaveListeners.has(target)) {
onDragLeaveListeners.get(target)!.push(onDragLeave);
} else {
onDragLeaveListeners.set(target, [onDragLeave]);
}
if (onDragOverListeners.has(target)) {
onDragOverListeners.get(target)!.push(onDragOver);
} else {
onDragOverListeners.set(target, [onDragOver]);
}
// https://stackoverflow.com/questions/14203734/dragend-dragenter-and-dragleave-firing-off-immediately-when-i-drag
setTimeout(() => {
target.classList.add(dz.active);
}, 10);
target.addEventListener('drop', onDrop);
target.addEventListener('dragenter', onDragEnter);
target.addEventListener('dragleave', onDragLeave);
target.addEventListener('dragover', onDragOver);
Array.from(dropzoneRegistry.values()).forEach((dropzone) => {
dropzone.register(opts.data);
});
// Get chromium to fire dragover & drop events
// https://stackoverflow.com/questions/6481094/html5-drag-and-drop-ondragover-not-firing-in-chrome/6483205#6483205
e.dataTransfer?.setData('text/html', 'd'); // cannot be empty string
e.dataTransfer?.setData('text/html', 'placeholder copy'); // cannot be empty string
e.dataTransfer?.setDragImage(clone, e.offsetX + 30, e.offsetY + 30); // Adds the padding
e.stopPropagation();
}
@ -201,25 +141,8 @@ export function draggable(node: HTMLElement, initialOpts: DraggableConfig) {
element.style.opacity = '1';
});
// deactivate destination zones
dzRegistry.forEach(async ([node, dz]) => {
if (!dz.accepts(await opts.data)) return;
// remove all listeners
onDropListeners.get(node)?.forEach((listener) => {
node.removeEventListener('drop', listener);
});
onDragEnterListeners.get(node)?.forEach((listener) => {
node.removeEventListener('dragenter', listener);
});
onDragLeaveListeners.get(node)?.forEach((listener) => {
node.removeEventListener('dragleave', listener);
});
onDragOverListeners.get(node)?.forEach((listener) => {
node.removeEventListener('dragover', listener);
});
node.classList.remove(dz.active);
node.classList.remove(dz.hover);
Array.from(dropzoneRegistry.values()).forEach((dropzone) => {
dropzone.unregister();
});
e.stopPropagation();

View File

@ -1,53 +1,183 @@
export interface Dropzone {
export interface DropzoneConfiguration {
disabled: boolean;
active: string;
hover: string;
accepts: (data: any) => boolean;
onDrop: (data: any) => Promise<void> | void;
onActivationStart: () => void;
onActivationEnd: () => void;
onHoverStart: () => void;
onHoverEnd: () => void;
target: string;
}
export class Dropzone {
private activated: boolean = false;
private hovered: boolean = false;
private registered: boolean = false;
// In order to propperly deregister functions we need to use the same
// reference so we must store the function after we bind the reference
private registeredOnDrop?: (e: DragEvent) => any;
private registeredOnDragEnter?: (e: DragEvent) => any;
private registeredOnDragLeave?: (e: DragEvent) => any;
private target!: HTMLElement;
private data?: any;
constructor(
private configuration: DropzoneConfiguration,
private rootNode: HTMLElement
) {
// Sets this.target
this.setTarget();
}
const defaultDropzoneOptions: Dropzone = {
disabled: false,
active: 'dropzone-active',
hover: 'dropzone-hover',
accepts: (data) => data === 'default',
onDrop: () => {}
};
async register(data: any) {
this.data = data;
export function dropzone(node: HTMLElement, opts: Partial<Dropzone> | undefined) {
let currentOptions = { ...defaultDropzoneOptions, ...opts };
if (!this.configuration.accepts(await this.data)) return;
if (this.registered) {
this.unregister();
}
this.registered = true;
function setup(opts: Partial<Dropzone> | undefined) {
currentOptions = { ...defaultDropzoneOptions, ...opts };
if (currentOptions.disabled) return;
this.registerListeners();
register(node, currentOptions);
// Mark the dropzone as active
setTimeout(() => {
this.configuration.onActivationStart();
this.activated = true;
}, 10);
}
unregister() {
// Mark as no longer active and ensure its not stuck in the hover state
this.activated = false;
this.configuration.onActivationEnd();
this.configuration.onHoverEnd();
this.unregisterListeners();
this.registered = false;
}
// Designed to quickly swap out the configuration
async reregister(configuration: DropzoneConfiguration) {
this.unregisterListeners();
// On the previous configuration, mark configuration as deactivated and unhovered
this.configuration.onActivationEnd();
this.configuration.onHoverEnd();
this.configuration = configuration;
this.setTarget();
if (!this.configuration.accepts(await this.data)) return;
if (this.hovered) {
this.configuration.onHoverStart();
} else {
this.configuration.onHoverEnd();
}
if (this.activated) {
this.configuration.onActivationStart();
} else {
this.configuration.onActivationEnd();
}
this.registerListeners();
}
private registerListeners() {
this.registeredOnDrop = this.onDrop.bind(this);
this.registeredOnDragEnter = this.onDragEnter.bind(this);
this.registeredOnDragLeave = this.onDragLeave.bind(this);
this.target.addEventListener('drop', this.registeredOnDrop);
this.target.addEventListener('dragenter', this.registeredOnDragEnter);
this.target.addEventListener('dragleave', this.registeredOnDragLeave);
}
private unregisterListeners() {
if (this.registeredOnDrop) {
this.target.removeEventListener('drop', this.registeredOnDrop);
}
if (this.registeredOnDragEnter) {
this.target.removeEventListener('dragenter', this.registeredOnDragEnter);
}
if (this.registeredOnDragLeave) {
this.target.removeEventListener('dragleave', this.registeredOnDragLeave);
}
}
private setTarget() {
const child = this.rootNode.querySelector<HTMLElement>(this.configuration.target);
if (child) {
this.target = child;
} else {
this.target = this.rootNode;
}
}
private async onDrop(e: DragEvent) {
e.preventDefault();
if (!this.activated) return;
this.configuration.onDrop(await this.data);
}
private onDragEnter(e: DragEvent) {
e.preventDefault();
if (!this.activated) return;
this.hovered = true;
this.configuration.onHoverStart();
}
private onDragLeave(e: DragEvent) {
e.preventDefault();
if (!this.activated) return;
this.hovered = false;
this.configuration.onHoverEnd();
}
}
export const dropzoneRegistry = new Map<HTMLElement, Dropzone>();
export function dropzone(node: HTMLElement, configuration: DropzoneConfiguration) {
function setup(configuration: DropzoneConfiguration) {
if (configuration.disabled) return;
if (dropzoneRegistry.has(node)) {
clean();
}
dropzoneRegistry.set(node, new Dropzone(configuration, node));
}
function clean() {
unregister(currentOptions);
dropzoneRegistry.get(node)?.unregister();
dropzoneRegistry.delete(node);
}
setup(opts);
setup(configuration);
function update(configuration: DropzoneConfiguration) {
const dropzone = dropzoneRegistry.get(node);
if (dropzone) {
dropzone.reregister(configuration);
} else {
setup(configuration);
}
}
return {
update(opts: Partial<Dropzone> | undefined) {
clean();
setup(opts);
update(configuration: DropzoneConfiguration) {
update(configuration);
},
destroy() {
clean();
}
};
}
export const dzRegistry: [HTMLElement, Dropzone][] = [];
function register(node: HTMLElement, dropzone: Dropzone) {
dzRegistry.push([node, dropzone]);
}
function unregister(dropzone: Dropzone) {
const index = dzRegistry.findIndex(([, dz]) => dz === dropzone);
if (index >= 0) dzRegistry.splice(index, 1);
}

View File

@ -1,73 +0,0 @@
import type { Commit } from '$lib/vbranches/types';
/**
* This class is used to determine how far a commit has been drag and dropped.
*
* We expect the dropzones to be in the following order:
*
* ```
* const indexer = new ReorderDropzoneIndexer(commits);
*
* <ReorderDropzone index={indexer.topDropzoneIndex} />
* <Commit id={commits[0].id} />
* <ReorderDropzone index={indexer.dropzoneIndexBelowCommit(commits[0].id)} />
* <Commit id={commits[1].id} />
* <ReorderDropzone index={indexer.dropzoneIndexBelowCommit(commits[1].id)} />
* ```
*/
export class ReorderDropzoneIndexer {
private dropzoneIndexes = new Map<string, number>();
private commitIndexes = new Map<string, number>();
constructor(commits: Commit[]) {
this.dropzoneIndexes.set('top', 0);
commits.forEach((commit, index) => {
this.dropzoneIndexes.set(commit.id, index + 1);
this.commitIndexes.set(commit.id, index);
});
}
get topDropzoneIndex() {
return this.dropzoneIndexes.get('top') ?? 0;
}
dropzoneIndexBelowCommit(commitId: string) {
const index = this.dropzoneIndexes.get(commitId);
if (index === undefined) {
throw new Error(`Commit ${commitId} not found in dropzoneIndexes`);
}
return index;
}
commitIndex(commitId: string) {
const index = this.commitIndexes.get(commitId);
if (index === undefined) {
throw new Error(`Commit ${commitId} not found in commitIndexes`);
}
return index;
}
/**
* A negative offset means the commit has been dragged up, and a positive offset means the commit has been dragged down.
*/
dropzoneCommitOffset(dropzoneIndex: number, commitId: string) {
const commitIndex = this.commitIndexes.get(commitId);
if (commitIndex === undefined) {
throw new Error(`Commit ${commitId} not found in commitIndexes`);
}
const offset = dropzoneIndex - commitIndex;
if (offset > 0) {
return offset - 1;
} else {
return offset;
}
}
}

View File

@ -0,0 +1,127 @@
import { DraggableCommit } from '$lib/dragging/draggables';
import type { BranchController } from '$lib/vbranches/branchController';
import type { Branch, Commit } from '$lib/vbranches/types';
// Exported for type access only
export class ReorderDropzone {
constructor(
private branchController: BranchController,
private branch: Branch,
private entry: Entry
) {}
accepts(data: any) {
if (!(data instanceof DraggableCommit)) return false;
if (data.branchId !== this.branch.id) return false;
if (this.entry.distanceToOtherCommit(data.commit.id) === 0) return false;
return true;
}
onDrop(data: any) {
if (!(data instanceof DraggableCommit)) return;
if (data.branchId !== this.branch.id) return;
const offset = this.entry.distanceToOtherCommit(data.commit.id);
this.branchController.reorderCommit(this.branch.id, data.commit.id, offset);
}
}
export class ReorderDropzoneManager {
private indexer: Indexer;
constructor(
private branchController: BranchController,
private branch: Branch,
commits: Commit[]
) {
this.indexer = new Indexer(commits);
}
get topDropzone() {
const entry = this.indexer.get('top');
return new ReorderDropzone(this.branchController, this.branch, entry);
}
dropzoneBelowCommit(commitId: string) {
const entry = this.indexer.get(commitId);
return new ReorderDropzone(this.branchController, this.branch, entry);
}
}
export class ReorderDropzoneManagerFactory {
constructor(private branchController: BranchController) {}
build(branch: Branch, commits: Commit[]) {
return new ReorderDropzoneManager(this.branchController, branch, commits);
}
}
// Private classes used to calculate distances between commtis
class Indexer {
private dropzoneIndexes = new Map<string, number>();
private commitIndexes = new Map<string, number>();
constructor(commits: Commit[]) {
this.dropzoneIndexes.set('top', 0);
commits.forEach((commit, index) => {
this.dropzoneIndexes.set(commit.id, index + 1);
this.commitIndexes.set(commit.id, index);
});
}
get(key: string) {
const index = this.getIndex(key);
return new Entry(this.commitIndexes, index);
}
private getIndex(key: string) {
if (key === 'top') {
return this.dropzoneIndexes.get(key) ?? 0;
} else {
const index = this.dropzoneIndexes.get(key);
if (index === undefined) {
throw new Error(`Commit ${key} not found in dropzoneIndexes`);
}
return index;
}
}
}
class Entry {
constructor(
private commitIndexes: Map<string, number>,
private index: number
) {}
/**
* A negative offset means the commit has been dragged up, and a positive offset means the commit has been dragged down.
*/
distanceToOtherCommit(commitId: string) {
const commitIndex = this.commitIndex(commitId);
const offset = this.index - commitIndex;
if (offset > 0) {
return offset - 1;
} else {
return offset;
}
}
private commitIndex(commitId: string) {
const index = this.commitIndexes.get(commitId);
if (index === undefined) {
throw new Error(`Commit ${commitId} not found in commitIndexes`);
}
return index;
}
}

View File

@ -1,13 +1,16 @@
<script lang="ts">
import { listen } from '$lib/backend/ipc';
import { Project } from '$lib/backend/projects';
import { BranchDragActionsFactory } from '$lib/branches/dragActions';
import { BranchService } from '$lib/branches/service';
import { CommitDragActionsFactory } from '$lib/commits/dragActions';
import History from '$lib/components/History.svelte';
import Navigation from '$lib/components/Navigation.svelte';
import NoBaseBranch from '$lib/components/NoBaseBranch.svelte';
import NotOnGitButlerBranch from '$lib/components/NotOnGitButlerBranch.svelte';
import ProblemLoadingRepo from '$lib/components/ProblemLoadingRepo.svelte';
import ProjectSettingsMenuAction from '$lib/components/ProjectSettingsMenuAction.svelte';
import { ReorderDropzoneManagerFactory } from '$lib/dragging/reorderDropzoneManager';
import { HistoryService } from '$lib/history/history';
import { persisted } from '$lib/persisted/persisted';
import * as events from '$lib/utils/events';
@ -30,7 +33,10 @@
baseBranchService,
gbBranchActive$,
branchService,
branchController
branchController,
branchDragActionsFactory,
commitDragActionsFactory,
reorderDropzoneManagerFactory
} = data);
$: branchesError = vbranchService.branchesError;
@ -45,6 +51,9 @@
$: setContext(BaseBranchService, baseBranchService);
$: setContext(BaseBranch, baseBranch);
$: setContext(Project, project);
$: setContext(BranchDragActionsFactory, branchDragActionsFactory);
$: setContext(CommitDragActionsFactory, commitDragActionsFactory);
$: setContext(ReorderDropzoneManagerFactory, reorderDropzoneManagerFactory);
const showHistoryView = persisted(false, 'showHistoryView');

View File

@ -1,5 +1,8 @@
import { invoke } from '$lib/backend/ipc';
import { BranchDragActionsFactory } from '$lib/branches/dragActions.js';
import { BranchService } from '$lib/branches/service';
import { CommitDragActionsFactory } from '$lib/commits/dragActions.js';
import { ReorderDropzoneManagerFactory } from '$lib/dragging/reorderDropzoneManager';
import { HistoryService } from '$lib/history/history';
import { getFetchNotifications } from '$lib/stores/fetches';
import { getHeads } from '$lib/stores/head';
@ -63,6 +66,10 @@ export async function load({ params, parent }) {
branchController
);
const branchDragActionsFactory = new BranchDragActionsFactory(branchController);
const commitDragActionsFactory = new CommitDragActionsFactory(branchController, project);
const reorderDropzoneManagerFactory = new ReorderDropzoneManagerFactory(branchController);
return {
authService,
baseBranchService,
@ -76,6 +83,9 @@ export async function load({ params, parent }) {
vbranchService,
// These observables are provided for convenience
gbBranchActive$
gbBranchActive$,
branchDragActionsFactory,
commitDragActionsFactory,
reorderDropzoneManagerFactory
};
}