Merge pull request #1510 from gitbutlerapp/add-draggable-action

Add draggable action
This commit is contained in:
Nikita Galaiko 2023-11-07 08:52:43 +01:00 committed by GitHub
commit 90db1ae8c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 427 additions and 305 deletions

View File

@ -0,0 +1,201 @@
export interface Dropzone {
disabled: boolean;
active: string;
hover: string;
accepts: (data: any) => boolean;
onDrop: (data: any) => Promise<void> | void;
}
const defaultDropzoneOptions: Dropzone = {
disabled: false,
active: 'dropzone-active',
hover: 'dropzone-hover',
accepts: (data) => data == 'default',
onDrop: () => {}
};
export function dropzone(node: HTMLElement, opts: Partial<Dropzone> | undefined) {
let currentOptions = { ...defaultDropzoneOptions, ...opts };
function handleDragEnter(e: DragEvent) {
if (activeZones.has(node)) {
node.classList.add(currentOptions.hover);
e.preventDefault();
}
}
function handleDragLeave(_e: DragEvent) {
if (activeZones.has(node)) {
node.classList.remove(currentOptions.hover);
}
}
function handleDragOver(e: DragEvent) {
if (activeZones.has(node)) {
e.preventDefault();
}
}
function setup(opts: Partial<Dropzone> | undefined) {
currentOptions = { ...defaultDropzoneOptions, ...opts };
if (currentOptions.disabled) return;
register(node, currentOptions);
node.addEventListener('dragenter', handleDragEnter);
node.addEventListener('dragleave', handleDragLeave);
node.addEventListener('dragover', handleDragOver);
}
function clean() {
unregister(currentOptions);
node.removeEventListener('dragenter', handleDragEnter);
node.removeEventListener('dragleave', handleDragLeave);
node.removeEventListener('dragover', handleDragOver);
}
setup(opts);
return {
update(opts: Partial<Dropzone> | undefined) {
clean();
setup(opts);
},
destroy() {
clean();
}
};
}
const registry: [HTMLElement, Dropzone][] = [];
const activeZones = new Set<HTMLElement>();
function register(node: HTMLElement, dropzone: Dropzone) {
registry.push([node, dropzone]);
}
function unregister(dropzone: Dropzone) {
const index = registry.findIndex(([, dz]) => dz == dropzone);
if (index >= 0) registry.splice(index, 1);
}
export interface Draggable {
data: any;
disabled: boolean;
}
const defaultDraggableOptions: Draggable = {
data: 'default',
disabled: false
};
export function draggable(node: HTMLElement, opts: Partial<Draggable> | undefined) {
let currentOptions = { ...defaultDraggableOptions, ...opts };
let clone: HTMLElement;
const onDropListeners = new Map<HTMLElement, Array<(e: DragEvent) => void>>();
/**
* The problem with the ghost element is that it gets clipped after rotation unless we enclose
* it within a larger bounding box. This means we have an extra `<div>` in the html that is
* only present to support the rotation
*/
function handleDragStart(e: DragEvent) {
// Start by cloning the node for the ghost element
clone = node.cloneNode(true) as HTMLElement;
clone.style.position = 'absolute';
clone.style.top = '-9999px'; // Element has to be in the DOM so we move it out of sight
clone.style.display = 'inline-block';
clone.style.padding = '30px'; // To prevent clipping of rotated element
// Style the inner node so it retains the shape and then rotate
const inner = clone.children[0] as HTMLElement;
inner.style.height = node.clientHeight + 'px';
inner.style.width = node.clientWidth + 'px';
inner.style.rotate = `${Math.floor(Math.random() * 3)}deg`;
document.body.appendChild(clone);
// Dim the original element while dragging
node.style.opacity = '0.6';
// activate destination zones
registry
.filter(([_node, dz]) => dz.accepts(currentOptions.data))
.forEach(([target, dz]) => {
const onDrop = (e: DragEvent) => {
e.preventDefault();
dz.onDrop(currentOptions.data);
};
// 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]);
}
// 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);
activeZones.add(target);
});
e.dataTransfer?.setDragImage(clone, e.offsetX + 30, e.offsetY + 30); // Adds the padding
e.stopPropagation();
}
function handleDragEnd(e: DragEvent) {
node.style.opacity = '1';
clone.remove();
// deactivate destination zones
registry
.filter(([_node, dz]) => dz.accepts(currentOptions.data))
.forEach(([node, dz]) => {
// remove all listeners
const onDrop = onDropListeners.get(node);
if (onDrop) {
onDrop.forEach((listener) => {
node.removeEventListener('drop', listener);
});
}
node.classList.remove(dz.active);
activeZones.delete(node);
});
e.stopPropagation();
}
function setup(opts: Partial<Draggable> | undefined) {
currentOptions = { ...defaultDraggableOptions, ...opts };
if (currentOptions.disabled) return;
node.draggable = true;
node.addEventListener('dragstart', handleDragStart);
node.addEventListener('dragend', handleDragEnd);
}
function clean() {
node.draggable = false;
node.removeEventListener('dragstart', handleDragStart);
node.removeEventListener('dragend', handleDragEnd);
}
setup(opts);
return {
update(opts: Partial<Draggable> | undefined) {
clean();
setup(opts);
},
destroy() {
clean();
}
};
}

View File

@ -1,175 +0,0 @@
const zoneMap = new Map<string, Set<HTMLElement>>();
const optionsMap = new Map<HTMLElement, DzOptions>();
export interface DzOptions {
type: string;
hover: string;
active: string;
}
const defaultOptions: DzOptions = {
hover: 'drop-zone-hover',
active: 'drop-zone-active',
type: 'default'
};
function inactivateZones(zones: Set<HTMLElement>) {
zones?.forEach((zone) => {
const opts = optionsMap.get(zone);
opts && zone.classList.remove(opts.active);
});
}
function activateZones(zones: Set<HTMLElement>, activeZone: HTMLElement) {
zones?.forEach((zone) => {
if (zone !== activeZone && !isChildOf(activeZone, zone)) {
const opts = optionsMap.get(zone);
opts && zone.classList.add(opts.active);
}
});
}
function getZones(type: string): Set<HTMLElement> {
let zones = zoneMap.get(type);
if (!zones) {
zones = new Set([]);
zoneMap.set(type, zones);
}
return zones;
}
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);
}
export function dzTrigger(node: HTMLElement, opts: Partial<DzOptions> | undefined) {
const options = { ...defaultOptions, ...opts };
const zones = getZones(options.type);
let clone: HTMLElement;
/**
* The problem with the ghost element is that it gets clipped after rotation unless we enclose
* it within a larger bounding box. This means we have an extra `<div>` in the html that is
* only present to support the rotation
*/
function handleDragStart(e: DragEvent) {
// Start by cloning the node for the ghost element
clone = node.cloneNode(true) as HTMLElement;
clone.style.position = 'absolute';
clone.style.top = '-9999px'; // Element has to be in the DOM so we move it out of sight
clone.style.display = 'inline-block';
clone.style.padding = '30px'; // To prevent clipping of rotated element
// Style the inner node so it retains the shape and then rotate
const inner = clone.children[0] as HTMLElement;
inner.style.height = node.clientHeight + 'px';
inner.style.width = node.clientWidth + 'px';
inner.style.rotate = `${Math.floor(Math.random() * 3)}deg`;
document.body.appendChild(clone);
// Dim the original element while dragging
node.style.opacity = '0.6';
e.dataTransfer?.setDragImage(clone, e.offsetX + 30, e.offsetY + 30); // Adds the padding
activateZones(zones, node);
e.stopPropagation();
}
function handleDragEnd(e: DragEvent) {
node.style.opacity = '1'; // Undo the dimming from `dragstart`
clone.remove(); // Remove temporary ghost element
e.stopPropagation();
inactivateZones(zones);
}
node.addEventListener('dragstart', handleDragStart);
node.addEventListener('dragend', handleDragEnd);
return {
destroy() {
node.removeEventListener('dragstart', handleDragStart);
node.removeEventListener('dragend', handleDragEnd);
}
};
}
export function dzHighlight(node: HTMLElement, opts: Partial<DzOptions> | undefined) {
const options = { ...defaultOptions, ...opts };
const zones = getZones(options.type);
zones.add(node);
optionsMap.set(node, options);
function setHover(value: boolean) {
if (value) {
// We do this so we can set pointer-events-none on all dropzones from main css file,
// without it onMouseLeave fires every time a child container is left.
node.classList.add(defaultOptions.hover);
node.classList.add(options.hover);
} else {
node.classList.remove(defaultOptions.hover);
node.classList.remove(options.hover);
}
}
function handleDragEnter(e: DragEvent) {
if (!e.dataTransfer?.types.includes(options.type)) {
return;
}
setHover(true);
e.stopPropagation();
}
function handleDragLeave(e: DragEvent) {
if (!e.dataTransfer?.types.includes(options.type)) {
return;
}
if (!isChildOf(e.target, node)) {
setHover(false);
}
e.stopPropagation();
}
function handleDragEnd(e: DragEvent) {
setHover(false);
inactivateZones(zones);
e.stopPropagation();
}
function handleDrop(e: DragEvent) {
if (!e.dataTransfer?.types.includes(options.type)) {
return;
}
setHover(false);
inactivateZones(zones);
}
function handleDragOver(e: DragEvent) {
if (!e.dataTransfer?.types.includes(options.type)) {
e.stopImmediatePropagation(); // Stops event from reaching `on:dragover` on the element
}
if (e.dataTransfer?.types.includes(options.type)) e.preventDefault();
}
node.addEventListener('dragend', handleDragEnd);
node.addEventListener('dragenter', handleDragEnter);
node.addEventListener('dragleave', handleDragLeave);
node.addEventListener('dragover', handleDragOver);
node.addEventListener('drop', handleDrop);
node.classList.add('drop-zone');
return {
destroy() {
node.removeEventListener('dragend', handleDragEnd);
node.removeEventListener('dragenter', handleDragEnter);
node.removeEventListener('dragleave', handleDragLeave);
node.removeEventListener('dragover', handleDragOver);
node.removeEventListener('drop', handleDrop);
zones?.delete(node);
}
};
}

View File

@ -32,9 +32,13 @@ export class BranchController {
}
}
async resetBranch(params: { branchId: string; projectId: string; targetCommitOid: string }) {
async resetBranch(branchId: string, targetCommitOid: string) {
try {
await invoke<void>('reset_virtual_branch', params);
await invoke<void>('reset_virtual_branch', {
branchId,
projectId: this.projectId,
targetCommitOid
});
await this.virtualBranchStore.reload();
} catch (err) {
toasts.error('Failed to reset branch');
@ -50,9 +54,14 @@ export class BranchController {
}
}
async commitBranch(params: { branch: string; message: string; ownership?: string }) {
async commitBranch(branch: string, message: string, ownership: string | undefined = undefined) {
try {
await invoke<void>('commit_virtual_branch', { projectId: this.projectId, ...params });
await invoke<void>('commit_virtual_branch', {
projectId: this.projectId,
branch,
message,
ownership
});
await this.virtualBranchStore.reload();
} catch (err) {
toasts.error('Failed to commit branch');
@ -160,9 +169,9 @@ export class BranchController {
await this.virtualBranchStore.reload();
}
async pushBranch(params: { branchId: string; withForce: boolean }) {
async pushBranch(branchId: string, withForce: boolean) {
try {
await invoke<void>('push_virtual_branch', { projectId: this.projectId, ...params });
await invoke<void>('push_virtual_branch', { projectId: this.projectId, branchId, withForce });
await this.virtualBranchStore.reload();
} catch (err: any) {
if (err.code === 'errors.git.authentication') {
@ -223,11 +232,12 @@ export class BranchController {
}
}
async cherryPick(params: { branchId: string; targetCommitOid: string }) {
async cherryPick(branchId: string, targetCommitOid: string) {
try {
await invoke<void>('cherry_pick_onto_virtual_branch', {
projectId: this.projectId,
...params
branchId,
targetCommitOid
});
await this.targetBranchStore.reload();
} catch (err: any) {
@ -235,12 +245,25 @@ export class BranchController {
}
}
async markResolved(projectId: string, path: string) {
async markResolved(path: string) {
try {
await invoke<void>('mark_resolved', { projectId, path });
await invoke<void>('mark_resolved', { projectId: this.projectId, path });
await this.virtualBranchStore.reload();
} catch (err) {
toasts.error(`Failed to mark file resolved`);
}
}
async amendBranch(branchId: string, ownership: string) {
try {
await invoke<void>('amend_virtual_branch', {
projectId: this.projectId,
branchId,
ownership
});
await this.targetBranchStore.reload();
} catch (err: any) {
toasts.error(`Failed to amend commit: ${err.message}`);
}
}
}

View File

@ -2,7 +2,6 @@
import BranchLane from '../components/BranchLane.svelte';
import NewBranchDropZone from './NewBranchDropZone.svelte';
import type { BaseBranch, Branch } from '$lib/vbranches/types';
import { dzHighlight } from '$lib/utils/dropZone';
import type { BranchController } from '$lib/vbranches/branchController';
import type { getCloudApiClient } from '$lib/backend/cloud';
import type { LoadState } from '@square/svelte-store';
@ -30,8 +29,6 @@
let priorPosition = 0;
let dropPosition = 0;
const dzType = 'text/branch';
function handleEmpty() {
const emptyIndex = branches?.findIndex((item) => !item.files || item.files.length == 0);
if (emptyIndex && emptyIndex != -1) {
@ -49,12 +46,14 @@
<div class="p-4">Something went wrong...</div>
{:else if branches}
<div
bind:this={dropZone}
id="branch-lanes"
class="bg-color-2 flex h-full flex-shrink flex-grow items-start"
role="group"
use:dzHighlight={{ type: dzType, active: 'board-dz-active', hover: 'board-dz-hover' }}
bind:this={dropZone}
on:dragover={(e) => {
if (!dragged) return;
e.preventDefault();
const children = [...e.currentTarget.children];
dropPosition = 0;
// We account for the NewBranchDropZone by subtracting 2
@ -73,8 +72,10 @@
: children[dropPosition].after(dragged);
}
}}
on:drop={() => {
on:drop={(e) => {
if (!dragged) return;
if (!branches) return;
e.preventDefault();
if (priorPosition != dropPosition) {
const el = branches.splice(priorPosition, 1);
branches.splice(dropPosition, 0, ...el);
@ -87,24 +88,31 @@
}}
>
{#each branches.filter((c) => c.active) as branch (branch.id)}
<BranchLane
<div
class="h-full"
role="group"
draggable="true"
on:dragstart={(e) => {
if (!e.dataTransfer) return;
e.dataTransfer.setData(dzType, branch.id);
dragged = e.currentTarget;
priorPosition = Array.from(dropZone.children).indexOf(dragged);
}}
on:empty={handleEmpty}
{branch}
{projectId}
{projectPath}
{base}
{cloudEnabled}
{cloud}
{branchController}
branchCount={branches.filter((c) => c.active).length}
{githubContext}
/>
on:dragend={() => {
dragged = undefined;
}}
>
<BranchLane
on:empty={handleEmpty}
{branch}
{projectId}
{projectPath}
{base}
{cloudEnabled}
{cloud}
{branchController}
branchCount={branches.filter((c) => c.active).length}
{githubContext}
/>
</div>
{/each}
{#if !activeBranches || activeBranches.length == 0}

View File

@ -1,20 +1,34 @@
<script lang="ts">
import { dzHighlight } from '$lib/utils/dropZone';
import { dropzone } from '$lib/utils/draggable';
import type { BranchController } from '$lib/vbranches/branchController';
import type { File, Hunk } from '$lib/vbranches/types';
export let branchController: BranchController;
function accepts(data: { hunk?: Hunk; file?: File }) {
if (data.hunk !== undefined) return true;
if (data.file !== undefined) return true;
return false;
}
function onDrop(data: { hunk?: Hunk; file?: File }) {
if (data.hunk) {
const ownership = `${data.hunk.filePath}:${data.hunk.id}`;
branchController.createBranch({ ownership });
} else if (data.file) {
const ownership = `${data.file.path}:${data.file.hunks.map(({ id }) => id).join(',')}`;
branchController.createBranch({ ownership });
}
}
</script>
<div
class="group h-full flex-grow p-2 font-semibold"
role="group"
use:dzHighlight={{ type: 'text/hunk', hover: 'new-dz-hover', active: 'new-dz-active' }}
on:drop|stopPropagation={(e) => {
if (!e.dataTransfer) {
return;
}
const ownership = e.dataTransfer.getData('text/hunk');
branchController.createBranch({ ownership });
use:dropzone={{
active: 'new-dz-active',
hover: 'new-dz-hover',
onDrop,
accepts
}}
>
<div

View File

@ -0,0 +1,44 @@
<script lang="ts">
import { dropzone } from '$lib/utils/draggable';
import type { Hunk, File, RemoteCommit } from '$lib/vbranches/types';
import type { BranchController } from '$lib/vbranches/branchController';
import CommitCard from './CommitCard.svelte';
export let branchController: BranchController;
export let branchId: string;
export let commit: RemoteCommit;
export let projectId: string;
function acceptBranchDrop(data: { branchId: string; file?: File; hunk?: Hunk }) {
if (data.branchId !== branchId) return false;
return !!data.file || !!data.hunk;
}
function onDrop(data: { file?: File; hunk?: Hunk }) {
if (data.hunk) {
const newOwnership = `${data.hunk.filePath}:${data.hunk.id}`;
branchController.amendBranch(branchId, newOwnership);
} else if (data.file) {
const newOwnership = `${data.file.path}:${data.file.hunks.map(({ id }) => id).join(',')}`;
branchController.amendBranch(branchId, newOwnership);
}
}
</script>
<div
class="relative h-full w-full"
use:dropzone={{
active: 'amend-dz-active',
hover: 'amend-dz-hover',
accepts: acceptBranchDrop,
onDrop: onDrop
}}
>
<div
class="amend-dz-marker absolute z-10 hidden h-full w-full items-center justify-center rounded bg-blue-100/70 outline-dashed outline-2 -outline-offset-8 outline-light-600 dark:bg-blue-900/60 dark:outline-dark-300"
>
<div class="hover-text font-semibold">Amend</div>
</div>
<CommitCard {commit} {projectId} />
</div>

View File

@ -1,12 +1,12 @@
<script lang="ts">
import { userStore } from '$lib/stores/user';
import type { BaseBranch, Branch } from '$lib/vbranches/types';
import type { BaseBranch, Branch, File, Hunk, RemoteCommit } from '$lib/vbranches/types';
import { getContext, onDestroy, onMount } from 'svelte';
import { draggable, dropzone } from '$lib/utils/draggable';
import { Ownership } from '$lib/vbranches/ownership';
import IconKebabMenu from '$lib/icons/IconKebabMenu.svelte';
import CommitCard from './CommitCard.svelte';
import { getExpandedWithCacheFallback, setExpandedWithCache } from './cache';
import { dzHighlight, dzTrigger } from '$lib/utils/dropZone';
import type { BranchController } from '$lib/vbranches/branchController';
import FileCard from './FileCard.svelte';
import { slide } from 'svelte/transition';
@ -39,6 +39,7 @@
import Button from '$lib/components/Button.svelte';
import Link from '$lib/components/Link.svelte';
import Modal from '$lib/components/Modal.svelte';
import AmendableCommitCard from './AmendableCommitCard.svelte';
const [send, receive] = crossfade({
duration: (d) => Math.sqrt(d * 200),
@ -88,7 +89,6 @@
let deleteBranchModal: Modal;
let applyConflictedModal: Modal;
const dzType = 'text/hunk';
const laneWidthKey = 'laneWidth:';
$: pullRequestPromise =
@ -119,7 +119,7 @@
async function push() {
isPushing = true;
await branchController.pushBranch({ branchId: branch.id, withForce: branch.requiresForce });
await branchController.pushBranch(branch.id, branch.requiresForce);
isPushing = false;
}
@ -232,17 +232,9 @@
function resetHeadCommit() {
if (branch.commits.length > 1) {
branchController.resetBranch({
projectId,
branchId: branch.id,
targetCommitOid: branch.commits[1].id
});
branchController.resetBranch(branch.id, branch.commits[1].id);
} else if (branch.commits.length === 1 && base) {
branchController.resetBranch({
projectId,
branchId: branch.id,
targetCommitOid: base?.baseSha
});
branchController.resetBranch(branch.id, base.baseSha);
}
}
@ -266,33 +258,40 @@
const selectedOwnership = writable(Ownership.fromBranch(branch));
$: if (commitDialogShown) selectedOwnership.set(Ownership.fromBranch(branch));
function acceptCherrypick(data: { branchId?: string; commit?: RemoteCommit }) {
return data?.branchId === branch.id && data.commit !== undefined;
}
function onCherrypicked(data: { branchId: string; commit: RemoteCommit }) {
branchController.cherryPick(branch.id, data.commit.id);
}
function acceptBranchDrop(data: { branchId: string; file?: File; hunk?: Hunk }) {
if (data.branchId === branch.id) return false; // can't drag to the same branch
if (data.hunk !== undefined && !data.hunk.locked) return true; // can only drag not locked hunks
if (data.file !== undefined && data.file.hunks.some((hunk) => !hunk.locked)) return true; // can only draged non fully locked files
return false;
}
function onBranchDrop(data: { file?: File; hunk?: Hunk }) {
if (data.hunk) {
const newOwnership = `${data.hunk.filePath}:${data.hunk.id}`;
branchController.updateBranchOwnership(
branch.id,
(newOwnership + '\n' + branch.ownership).trim()
);
} else if (data.file) {
const newOwnership = `${data.file.path}:${data.file.hunks.map(({ id }) => id).join(',')}`;
branchController.updateBranchOwnership(
branch.id,
(newOwnership + '\n' + branch.ownership).trim()
);
}
}
</script>
<div
class="flex h-full shrink-0 snap-center"
style:width={maximized ? '100%' : `${laneWidth}px`}
draggable={!readonly}
role="group"
use:dzHighlight={{ type: dzType, hover: 'lane-dz-hover', active: 'lane-dz-active' }}
on:dragstart
on:dragend
on:drop={(e) => {
if (!e.dataTransfer) {
return;
}
const data = e.dataTransfer.getData(dzType);
const [newFileId, newHunks] = data.split(':');
const existingHunkIds =
branch.files.find((f) => f.id === newFileId)?.hunks.map((h) => h.id) || [];
const newHunkIds = newHunks.split(',').filter((h) => !existingHunkIds.includes(h));
if (newHunkIds.length == 0) {
// don't allow dropping hunk to the lane where it already is
return;
}
e.stopPropagation();
branchController.updateBranchOwnership(branch.id, (data + '\n' + branch.ownership).trim());
}}
>
<div class="flex h-full shrink-0 snap-center" style:width={maximized ? '100%' : `${laneWidth}px`}>
<div
bind:this={rsViewport}
class="bg-color-3 border-color-4 flex flex-grow cursor-default flex-col overflow-x-hidden border-l border-r"
@ -432,16 +431,11 @@
</div>
{#if upstreamCommitsShown}
<div
class="flex w-full flex-col border-t border-light-400 bg-light-300 p-2 dark:border-dark-400 dark:bg-dark-800"
class="flex w-full flex-col gap-1 border-t border-light-400 bg-light-300 p-2 dark:border-dark-400 dark:bg-dark-800"
id="upstreamCommits"
>
{#each branch.upstream.commits as commit}
<div
role="group"
draggable="true"
use:dzTrigger={{ type: 'commit/upstream' }}
on:dragstart={(e) => e.dataTransfer?.setData('commit/upstream', commit.id)}
>
{#each branch.upstream.commits as commit (commit.id)}
<div use:draggable={{ data: { branchId: branch.id, commit } }}>
<CommitCard {commit} {projectId} />
</div>
{/each}
@ -488,25 +482,17 @@
/>
<div
class="relative flex flex-grow overflow-y-hidden"
use:dzHighlight={{
type: 'commit/upstream',
use:dropzone={{
hover: 'cherrypick-dz-hover',
active: 'cherrypick-dz-active'
active: 'cherrypick-dz-active',
accepts: acceptCherrypick,
onDrop: onCherrypicked
}}
role="group"
on:drop={(e) => {
if (!e.dataTransfer) {
return;
}
const targetCommitOid = e.dataTransfer.getData('commit/upstream');
if (!targetCommitOid) {
return;
}
e.stopPropagation();
branchController.cherryPick({
targetCommitOid,
branchId: branch.id
});
use:dropzone={{
hover: 'lane-dz-hover',
active: 'lane-dz-active',
accepts: acceptBranchDrop,
onDrop: onBranchDrop
}}
>
<!-- TODO: Figure out why z-10 is necessary for expand up/down to not come out on top -->
@ -550,9 +536,8 @@
expanded={file.expanded}
conflicted={file.conflicted}
{selectedOwnership}
branchId={branch.id}
{file}
{dzType}
{projectId}
{projectPath}
{branchController}
selectable={commitDialogShown}
@ -664,7 +649,16 @@
<div class="border-color-4 h-3 w-3 rounded-full border-2" />
</div>
{/if}
<CommitCard {projectId} {commit} />
{#if branch.commits.at(0)?.id === commit.id}
<AmendableCommitCard
{branchController}
branchId={branch.id}
{commit}
{projectId}
/>
{:else}
<CommitCard {commit} {projectId} />
{/if}
</div>
{/each}
</div>
@ -751,7 +745,16 @@
/>
</div>
{/if}
<CommitCard {projectId} {commit} />
{#if branch.commits.at(0)?.id === commit.id}
<AmendableCommitCard
{branchController}
branchId={branch.id}
{commit}
{projectId}
/>
{:else}
<CommitCard {commit} {projectId} />
{/if}
</div>
{/each}
</div>

View File

@ -32,7 +32,9 @@
}
</script>
<div class="text-color-2 bg-color-5 border-color-4 w-full truncate rounded border p-2 text-left">
<div
class="text-color-2 bg-color-5 border-color-4 relative w-full truncate rounded border p-2 text-left"
>
<div class="mb-1 flex justify-between">
<div class="truncate">
<button
@ -150,3 +152,13 @@
</div>
</svelte:fragment>
</Modal>
<style lang="postcss">
/* amend drop zone */
:global(.amend-dz-active .amend-dz-marker) {
@apply flex;
}
:global(.amend-dz-hover .hover-text) {
@apply visible;
}
</style>

View File

@ -31,11 +31,7 @@
Math.min(Math.max(commitMessage ? commitMessage.split('\n').length : 0, 1), 10) + 2;
function commit() {
branchController.commitBranch({
branch: branch.id,
message: commitMessage,
ownership: ownership.toString()
});
branchController.commitBranch(branch.id, commitMessage, ownership.toString());
}
export function git_get_config(params: { key: string }) {

View File

@ -2,6 +2,7 @@
import { ContentSection, HunkSection, parseFileSections } from './fileSections';
import { createEventDispatcher, onDestroy } from 'svelte';
import type { File, Hunk } from '$lib/vbranches/types';
import { draggable } from '$lib/utils/draggable';
import type { Ownership } from '$lib/vbranches/ownership';
import type { Writable } from 'svelte/store';
import RenderedLine from './RenderedLine.svelte';
@ -14,7 +15,6 @@
} from '$lib/icons';
import type { BranchController } from '$lib/vbranches/branchController';
import { getContext } from 'svelte';
import { dzTrigger } from '$lib/utils/dropZone';
import IconExpandUpDownSlim from '$lib/icons/IconExpandUpDownSlim.svelte';
import { getVSIFileIcon } from '$lib/ext-icons';
import { slide } from 'svelte/transition';
@ -24,10 +24,9 @@
import IconLock from '$lib/icons/IconLock.svelte';
import HunkContextMenu from './HunkContextMenu.svelte';
export let branchId: string;
export let file: File;
export let conflicted: boolean;
export let projectId: string;
export let dzType: string;
export let projectPath: string | undefined;
export let expanded: boolean | undefined;
export let branchController: BranchController;
@ -77,10 +76,6 @@
$: minWidth = getGutterMinWidth(maxLineNumber);
function getAllHunksOwnership(): string {
return file.id + ':' + file.hunks.map((h) => h.id).join(',');
}
$: isFileLocked = sections
.filter((section): section is HunkSection => section instanceof HunkSection)
.some((section) => section.hunk.locked);
@ -100,10 +95,10 @@
<div
id={`file-${file.id}`}
draggable={!isFileLocked && !readonly}
use:dzTrigger={{ type: dzType }}
on:dragstart={(e) => e.dataTransfer?.setData('text/hunk', getAllHunksOwnership())}
role="group"
use:draggable={{
data: { branchId, file },
disabled: readonly
}}
class="changed-file inner"
class:opacity-80={isFileLocked}
>
@ -164,7 +159,7 @@
<div class="mb-2 bg-red-500 px-2 py-0 font-bold text-white">
<button
class="font-bold text-white"
on:click={() => branchController.markResolved(projectId, file.path)}
on:click={() => branchController.markResolved(file.path)}
>
Mark resolved
</button>
@ -194,13 +189,14 @@
class="bg-6 border-color-3 my-1 flex w-full flex-col overflow-hidden rounded border"
>
<div
draggable={!section.hunk.locked && !readonly}
tabindex="0"
role="cell"
use:dzTrigger={{ type: dzType }}
on:dragstart={(e) => {
if ('hunk' in section)
e.dataTransfer?.setData('text/hunk', file.id + ':' + section.hunk.id);
use:draggable={{
data: {
branchId,
hunk: section.hunk
},
disabled: readonly
}}
on:dblclick
class="changed-hunk"