mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-02 07:53:55 +03:00
Merge pull request #1510 from gitbutlerapp/add-draggable-action
Add draggable action
This commit is contained in:
commit
90db1ae8c3
201
packages/ui/src/lib/utils/draggable.ts
Normal file
201
packages/ui/src/lib/utils/draggable.ts
Normal 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();
|
||||
}
|
||||
};
|
||||
}
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 }) {
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user